Add support for http, https, and file URLs.
authorTimothy B. Terriberry <tterribe@xiph.org>
Sat, 22 Sep 2012 15:54:04 +0000 (08:54 -0700)
committerTimothy B. Terriberry <tterribe@xiph.org>
Sat, 22 Sep 2012 15:54:04 +0000 (08:54 -0700)
This is pretty preliminary for now.
Seeking performance isn't great, and there's no caching.
I'm sure there's also lots of bugs.

examples/opusfile_example.c
include/opusfile.h
src/http.c [new file with mode: 0644]
src/opusfile.c
unix/Makefile

index 99240ba..6232324 100644 (file)
@@ -38,19 +38,23 @@ int main(int _argc,const char **_argv){
     OpusFileCallbacks cb={NULL,NULL,NULL,NULL};
     of=op_open_callbacks(op_fdopen(&cb,fileno(stdin),"rb"),&cb,NULL,0,&ret);
   }
-#if 0
-  /*For debugging: force a file to not be seekable.*/
   else{
-    OpusFileCallbacks  cb={NULL,NULL,NULL,NULL};
-    void              *fp;
-    fp=op_fopen(&cb,_argv[1],"rb");
-    cb.seek=NULL;
-    cb.tell=NULL;
-    of=op_open_callbacks(fp,&cb,NULL,0,NULL);
-  }
+    /*Try to treat the argument as a URL.*/
+    of=op_open_url(_argv[1],OP_SSL_SKIP_CERTIFICATE_CHECK,&ret);
+#if 0
+    if(of==NULL){
+        OpusFileCallbacks  cb={NULL,NULL,NULL,NULL};
+        void              *fp;
+        /*For debugging: force a file to not be seekable.*/
+        fp=op_fopen(&cb,_argv[1],"rb");
+        cb.seek=NULL;
+        cb.tell=NULL;
+        of=op_open_callbacks(fp,&cb,NULL,0,NULL);
+      }
 #else
-  else of=op_open_file(_argv[1],&ret);
+    if(of==NULL)of=op_open_file(_argv[1],&ret);
 #endif
+  }
   if(of==NULL){
     fprintf(stderr,"Failed to open file '%s': %i\n",_argv[1],ret);
     return EXIT_FAILURE;
index 5e88a2b..908be51 100644 (file)
@@ -55,6 +55,15 @@ typedef struct OggOpusFile OggOpusFile;
 #  define OP_ARG_NONNULL(_x)
 # endif
 
+/**\defgroup error_codes Error Codes*/
+/*@{*/
+/**\name List of possible error codes
+   Many of the functions in this library return a negative error code when a
+    function fails.
+   This list provides a brief explanation of the common errors.
+   See each individual function for more details on what a specific error code
+    means in that context.*/
+/*@{*/
 /**A request did not succeed.*/
 #define OP_FALSE         (-1)
 /*Currently not used externally.*/
@@ -97,6 +106,11 @@ typedef struct OggOpusFile OggOpusFile;
 #define OP_ENOSEEK       (-138)
 /**The first or last granule position of a link failed basic validity checks.*/
 #define OP_EBADTIMESTAMP (-139)
+/*@}*/
+/*@}*/
+
+/**\defgroup header_info Header Information*/
+/*@{*/
 
 /**The maximum number of channels in an Ogg Opus stream.*/
 #define OPUS_CHANNEL_COUNT_MAX (255)
@@ -337,6 +351,23 @@ void opus_tags_clear(OpusTags *_tags) OP_ARG_NONNULL(1);
 
 /*@}*/
 
+/*@}*/
+
+/**\defgroup url_flags URL Reading Flags*/
+/*@{*/
+/**\name Flags for op_url_create_with_proxy() and associated functions.
+   These may be expanded in the future.*/
+/*@{*/
+
+/**Skip the certificate check when connecting via TLS/SSL (https).*/
+#define OP_SSL_SKIP_CERTIFICATE_CHECK (1)
+
+/*@}*/
+/*@}*/
+
+/*\defgroup file_callbacks Abstract Stream Reading Functions*/
+/*@{*/
+
 /**\name Functions for reading from streams
    These functions define the interface used to read from and seek in a stream
     of data.
@@ -418,7 +449,8 @@ struct OpusFileCallbacks{
                       filled in here.
    \param      _path The path to the file to open.
    \param      _mode The mode to open the file in.
-   \return A stream handle to use with the callbacks, or NULL on error.*/
+   \return A stream handle to use with the callbacks, or <code>NULL</code> on
+            error.*/
 OP_WARN_UNUSED_RESULT void *op_fopen(OpusFileCallbacks *_cb,
  const char *_path,const char *_mode) OP_ARG_NONNULL(1) OP_ARG_NONNULL(2)
  OP_ARG_NONNULL(3);
@@ -434,7 +466,8 @@ OP_WARN_UNUSED_RESULT void *op_fopen(OpusFileCallbacks *_cb,
                       filled in here.
    \param      _fd   The file descriptor to open.
    \param      _mode The mode to open the file in.
-   \return A stream handle to use with the callbacks, or NULL on error.*/
+   \return A stream handle to use with the callbacks, or <code>NULL</code> on
+            error.*/
 OP_WARN_UNUSED_RESULT void *op_fdopen(OpusFileCallbacks *_cb,
  int _fd,const char *_mode) OP_ARG_NONNULL(1) OP_ARG_NONNULL(3);
 
@@ -451,7 +484,8 @@ OP_WARN_UNUSED_RESULT void *op_fdopen(OpusFileCallbacks *_cb,
    \param      _mode   The mode to open the file in.
    \param      _stream A stream previously returned by op_fopen(), op_fdopen(),
                         or op_freopen().
-   \return A stream handle to use with the callbacks, or NULL on error.*/
+   \return A stream handle to use with the callbacks, or <code>NULL</code> on
+            error.*/
 OP_WARN_UNUSED_RESULT void *op_freopen(OpusFileCallbacks *_cb,
  const char *_path,const char *_mode,void *_stream) OP_ARG_NONNULL(1)
  OP_ARG_NONNULL(2) OP_ARG_NONNULL(3) OP_ARG_NONNULL(4);
@@ -464,9 +498,65 @@ OP_WARN_UNUSED_RESULT void *op_freopen(OpusFileCallbacks *_cb,
                       filled in here.
    \param      _data The block of memory to read from.
    \param      _size The size of the block of memory.
-   \return A stream handle to use with the callbacks, or NULL on error.*/
+   \return A stream handle to use with the callbacks, or <code>NULL</code> on
+            error.*/
 OP_WARN_UNUSED_RESULT void *op_mem_stream_create(OpusFileCallbacks *_cb,
- const unsigned char *_data,size_t _size);
+ const unsigned char *_data,size_t _size) OP_ARG_NONNULL(1);
+
+/**Creates a stream that reads from the given URL.
+   This is equivalent to calling op_url_stream_create_with_proxy() with
+    <code>NULL</code> for \a _proxy_host.
+   \param[out] _cb    The callbacks to use for this stream.
+                      If there is an error creating the stream, nothing will be
+                       filled in here.
+   \param      _url   The URL to read from.
+                      Currently only the "file:", "http:", and "https:" schemes
+                       are supported.
+                      Both "http:" and "https:" may be disabled at compile
+                       time, in which case opening such URLs will fail.
+   \param      _flags The <a href="#url_flags">optional flags</a> to use.
+   \return A stream handle to use with the callbacks, or <code>NULL</code> on
+            error.*/
+OP_WARN_UNUSED_RESULT void *op_url_stream_create(OpusFileCallbacks *_cb,
+ const char *_url,int _flags) OP_ARG_NONNULL(1) OP_ARG_NONNULL(2);
+
+/**Creates a stream that reads from the given URL using the specified proxy.
+   \param[out] _cb         The callbacks to use for this stream.
+                           If there is an error creating the stream, nothing
+                            will be filled in here.
+   \param      _url        The URL to read from.
+                           Currently only the "file:", "http:", and "https:"
+                            schemes are supported.
+                           Both "http:" and "https:" may be disabled at compile
+                            time, in which case opening such URLs will fail.
+   \param      _flags      The <a href="#url_flags">optional flags</a> to use.
+   \param      _proxy_host The host of the proxy to connect to.
+                           This may be <code>NULL</code> if you do not wish to
+                            use a proxy.
+                           The proxy information is ignored if \a _url is a
+                            <file:> URL.
+   \param      _proxy_port The port of the proxy to connect to.
+                           This is ignored if \a _proxy_host is
+                            <code>NULL</code>.
+   \param      _proxy_user The username to use with the specified proxy.
+                           This may be <code>NULL</code> if no authorization is
+                            required.
+                           This is ignored if \a _proxy_host is
+                            <code>NULL</code>.
+   \param      _proxy_pass The password to use with the specified proxy.
+                           This may be <code>NULL</code> if no authorization is
+                            required.
+                           This is ignored if either \a _proxy_host or
+                            \a _proxy_user are <code>NULL</code>.
+   \return A stream handle to use with the callbacks, or <code>NULL</code> on
+            error.*/
+OP_WARN_UNUSED_RESULT void *op_url_stream_create_with_proxy(
+ OpusFileCallbacks *_cb,const char *_url,int _flags,
+  const char *_proxy_host,unsigned _proxy_port,
+  const char *_proxy_user,const char *_proxy_pass) OP_ARG_NONNULL(1)
+  OP_ARG_NONNULL(2);
+
+/*@}*/
 
 /*@}*/
 
@@ -510,11 +600,12 @@ int op_test(OpusHead *_head,
 /**Open a stream from the given file path.
    \param      _path  The path to the file to open.
    \param[out] _error Returns 0 on success, or a failure code on error.
-                      You may pass in NULL if you don't want the failure code.
+                      You may pass in <code>NULL</code> if you don't want the
+                       failure code.
                       The failure code will be #OP_EFAULT if the file could not
                        be opened, or one of the other failure codes from
                        op_open_callbacks() otherwise.
-   \return An #OggOpusFile pointer on success, or NULL on error.*/
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
 OP_WARN_UNUSED_RESULT OggOpusFile *op_open_file(const char *_path,int *_error)
  OP_ARG_NONNULL(1);
 
@@ -522,12 +613,64 @@ OP_WARN_UNUSED_RESULT OggOpusFile *op_open_file(const char *_path,int *_error)
    \param      _data  The memory buffer to open.
    \param      _size  The number of bytes in the buffer.
    \param[out] _error Returns 0 on success, or a failure code on error.
-                      You may pass in NULL if you don't want the failure code.
+                      You may pass in <code>NULL</code> if you don't want the
+                       failure code.
                       See op_open_callbacks() for a full list of failure codes.
-   \return An #OggOpusFile pointer on success, or NULL on error.*/
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
 OP_WARN_UNUSED_RESULT OggOpusFile *op_open_memory(const unsigned char *_data,
  size_t _size,int *_error);
 
+/**Open a stream from a URL.
+   \param      _url   The URL to open.
+                      Currently only the <file:>, <http:>, and <https:> schemes
+                       are supported.
+                      Both "http:" and "https:" may be disabled at compile
+                       time, in which case opening such URLs will fail.
+   \param      _flags The <a href="#url_flags">optional flags</a> to use.
+   \param[out] _error Returns 0 on success, or a failure code on error.
+                      You may pass in <code>NULL</code> if you don't want the
+                       failure code.
+                      See op_open_callbacks() for a full list of failure codes.
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
+OP_WARN_UNUSED_RESULT OggOpusFile *op_open_url(const char *_url,
+ int _flags,int *_error) OP_ARG_NONNULL(1);
+
+/**Open a stream from a URL using the specified proxy.
+   \param      _url        The URL to open.
+                           Currently only the <file:>, <http:>, and <https:>
+                            schemes are supported.
+                           Both "http:" and "https:" may be disabled at compile
+                            time, in which case opening such URLs will fail.
+   \param      _flags      The <a href="#url_flags">optional flags</a> to use.
+   \param      _proxy_host The host of the proxy to connect to.
+                           This may be <code>NULL</code> if you do not wish to
+                            use a proxy.
+                           The proxy information is ignored if \a _url is a
+                            <file:> URL.
+   \param      _proxy_port The port of the proxy to connect to.
+                           This is ignored if \a _proxy_host is
+                            <code>NULL</code>.
+   \param      _proxy_user The username to use with the specified proxy.
+                           This may be <code>NULL</code> if no authorization is
+                            required.
+                           This is ignored if \a _proxy_host is
+                            <code>NULL</code>.
+   \param      _proxy_pass The password to use with the specified proxy.
+                           This may be <code>NULL</code> if no authorization is
+                            required.
+                           This is ignored if either \a _proxy_host or
+                            \a _proxy_user are <code>NULL</code>.
+   \param[out] _error      Returns 0 on success, or a failure code on error.
+                           You may pass in <code>NULL</code> if you don't want
+                            the failure code.
+                           See op_open_callbacks() for a full list of failure
+                            codes.
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
+OP_WARN_UNUSED_RESULT OggOpusFile *op_open_url_with_proxy(const char *_url,
+ int _flags,const char *_proxy_host,unsigned _proxy_port,
+ const char *_proxy_user,const char *_proxy_pass,int *_error)
+ OP_ARG_NONNULL(1);
+
 /**Open a stream using the given set of callbacks to access it.
    \param _source        The stream to read from (e.g., a <code>FILE *</code>).
    \param _cb            The callbacks with which to access the stream.
@@ -562,8 +705,8 @@ OP_WARN_UNUSED_RESULT OggOpusFile *op_open_memory(const unsigned char *_data,
                          Otherwise, seeking to absolute positions will
                           generate inconsistent results.
    \param[out] _error    Returns 0 on success, or a failure code on error.
-                         You may pass in NULL if you don't want the failure
-                          code.
+                         You may pass in <code>NULL</code> if you don't want
+                          the failure code.
                          The failure code will be one of
                          <dl>
                            <dt>#OP_EREAD</dt>
@@ -609,26 +752,83 @@ OP_WARN_UNUSED_RESULT OggOpusFile *op_open_callbacks(void *_source,
 void op_free(OggOpusFile *_of);
 
 /**Partially open a stream from the given file path.
+   \see op_test_callbacks
    \param      _path  The path to the file to open.
    \param[out] _error Returns 0 on success, or a failure code on error.
-                      You may pass in NULL if you don't want the failure code.
+                      You may pass in <code>NULL</code> if you don't want the
+                       failure code.
                       The failure code will be #OP_EFAULT if the file could not
                        be opened, or one of the other failure codes from
                        op_open_callbacks() otherwise.
-   \return An #OggOpusFile pointer on success, or NULL on error.*/
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
 OP_WARN_UNUSED_RESULT OggOpusFile *op_test_file(const char *_path,int *_error)
  OP_ARG_NONNULL(1);
 
 /**Partially open a stream from a memory buffer.
+   \see op_test_callbacks
    \param      _data  The memory buffer to open.
    \param      _size  The number of bytes in the buffer.
    \param[out] _error Returns 0 on success, or a failure code on error.
-                      You may pass in NULL if you don't want the failure code.
+                      You may pass in <code>NULL</code> if you don't want the
+                       failure code.
                       See op_open_callbacks() for a full list of failure codes.
-   \return An #OggOpusFile pointer on success, or NULL on error.*/
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
 OP_WARN_UNUSED_RESULT OggOpusFile *op_test_memory(const unsigned char *_data,
  size_t _size,int *_error);
 
+/**Partially open a stream from a URL.
+   \see op_test_callbacks
+   \param      _url   The URL to open.
+                      Currently only the <file:>, <http:>, and <https:> schemes
+                       are supported.
+                      Both "http:" and "https:" may be disabled at compile
+                       time, in which case opening such URLs will fail.
+   \param      _flags The <a href="#url_flags">optional flags</a> to use.
+   \param[out] _error Returns 0 on success, or a failure code on error.
+                      You may pass in <code>NULL</code> if you don't want the
+                       failure code.
+                      See op_open_callbacks() for a full list of failure codes.
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
+OP_WARN_UNUSED_RESULT OggOpusFile *op_test_url(const char *_url,int _flags,
+ int *_error) OP_ARG_NONNULL(1);
+
+/**Partially open a stream from a URL using the specified proxy.
+   \see op_test_callbacks
+   \param      _url        The URL to open.
+                           Currently only the <file:>, <http:>, and <https:>
+                            schemes are supported.
+                           Both "http:" and "https:" may be disabled at compile
+                            time, in which case opening such URLs will fail.
+   \param      _flags      The <a href="#url_flags">optional flags</a> to use.
+   \param      _proxy_host The host of the proxy to connect to.
+                           This may be <code>NULL</code> if you do not wish to
+                            use a proxy.
+                           The proxy information is ignored if \a _url is a
+                            <file:> URL.
+   \param      _proxy_port The port of the proxy to connect to.
+                           This is ignored if \a _proxy_host is
+                            <code>NULL</code>.
+   \param      _proxy_user The username to use with the specified proxy.
+                           This may be <code>NULL</code> if no authorization is
+                            required.
+                           This is ignored if \a _proxy_host is
+                            <code>NULL</code>.
+   \param      _proxy_pass The password to use with the specified proxy.
+                           This may be <code>NULL</code> if no authorization is
+                            required.
+                           This is ignored if either \a _proxy_host or
+                            \a _proxy_user are <code>NULL</code>.
+   \param[out] _error      Returns 0 on success, or a failure code on error.
+                           You may pass in <code>NULL</code> if you don't want
+                            the failure code.
+                           See op_open_callbacks() for a full list of failure
+                            codes.
+   \return An #OggOpusFile pointer on success, or <code>NULL</code> on error.*/
+OP_WARN_UNUSED_RESULT OggOpusFile *op_test_url_with_proxy(const char *_url,
+ int _flags,const char *_proxy_host,unsigned _proxy_port,
+ const char *_proxy_user,const char *_proxy_pass,int *_error)
+ OP_ARG_NONNULL(1);
+
 /**Partially open a stream using the given set of callbacks to access it.
    This tests for Opusness and loads the headers for the first link.
    It does not seek (although it tests for seekability).
@@ -668,8 +868,8 @@ OP_WARN_UNUSED_RESULT OggOpusFile *op_test_memory(const unsigned char *_data,
                          Otherwise, seeking to absolute positions will
                           generate inconsistent results.
    \param[out] _error    Returns 0 on success, or a failure code on error.
-                         You may pass in NULL if you don't want the failure
-                          code.
+                         You may pass in <code>NULL</code> if you don't want
+                          the failure code.
                          See op_open_callbacks() for a full list of failure
                           codes.
    \return A freshly opened #OggOpusFile, or <code>NULL</code> on error.*/
@@ -712,7 +912,7 @@ int op_link_count(OggOpusFile *_of) OP_ARG_NONNULL(1);
 
 /**Returns whether or not the data source being read is seekable.
    This is true if
-   a) The seek and tell callbacks are both non-NULL,
+   a) The seek and tell callbacks are both non-<code>NULL</code>,
    b) The seek callback was successfully executed at least once, and
    c) The tell callback was successfully able to report the position indicator
     afterwards.
@@ -913,7 +1113,8 @@ ogg_int64_t op_pcm_tell(OggOpusFile *_of) OP_ARG_NONNULL(1);
                          Smaller buffers will simply return less data, possibly
                           consuming more memory to buffer the data internally.
    \param[out] _li       The index of the link this data was decoded from.
-                         You may pass NULL if you do not need this information.
+                         You may pass <code>NULL</code> if you do not need this
+                          information.
                          If this function fails (returning a negative value),
                           this parameter is left unset.
    \return The number of samples read per channel on success, or a negative
@@ -963,7 +1164,8 @@ OP_WARN_UNUSED_RESULT int op_read(OggOpusFile *_of,
                          Smaller buffers will simply return less data, possibly
                           consuming more memory to buffer the data internally.
    \param[out] _li       The index of the link this data was decoded from.
-                         You may pass NULL if you do not need this information.
+                         You may pass <code>NULL</code> if you do not need this
+                          information.
                          If this function fails (returning a negative value),
                           this parameter is left unset.
    \return The number of samples read per channel on success, or a negative
diff --git a/src/http.c b/src/http.c
new file mode 100644 (file)
index 0000000..5be9ebe
--- /dev/null
@@ -0,0 +1,1727 @@
+#include "internal.h"
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <string.h>
+
+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;
+  ret=(char *)_ogg_malloc(sizeof(*ret)*(len+1));
+  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));
+}
+
+/*Is this an https URL?
+  For now we can simply check the last letter.*/
+#define OP_URL_IS_SSL(_url) ((_url)->scheme[4]=='s')
+
+#define OP_URL_IS_DEFAULT_PORT(_url) \
+ (!OP_URL_IS_SSL(_url)&&(_url)->port==80 \
+ ||OP_URL_IS_SSL(_url)&&(_url)->port==443)
+
+/*URI character classes (from RFC 3986).*/
+#define OP_URL_ALPHA \
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+#define OP_URL_DIGIT       "01234567890"
+#define OP_URL_HEXDIGIT    "01234567890ABCDEFabcdef"
+/*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;
+    const char *host_end;
+    char        host_buf[28];
+    /*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;
+    /*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);
+    /*Some other host: give up.*/
+    if(OP_UNLIKELY(op_strncasecmp(host_buf,"localhost",9)!=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)
+# include <sys/types.h>
+# include <sys/socket.h>
+# include <sys/time.h>
+# include <arpa/inet.h>
+# include <fcntl.h>
+# include <netinet/in.h>
+# include <netdb.h>
+# include <poll.h>
+# include <unistd.h>
+# include <openssl/ssl.h>
+
+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;
+}
+
+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.*/
+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  *fragment_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{
+    user=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=fragment_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(*path_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);
+}
+
+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;
+}
+
+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_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;
+}
+
+struct OpusHTTPConn{
+  /*The current position indicator for this connection.*/
+  opus_int64    pos;
+  SSL          *ssl_conn;
+  OpusHTTPConn *next;
+  int           fd;
+};
+
+static void op_http_conn_init(OpusHTTPConn *_conn){
+  _conn->ssl_conn=NULL;
+  _conn->next=NULL;
+  _conn->fd=-1;
+}
+
+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!=-1)close(_conn->fd);
+}
+
+/*The maximum number of simultaneous connections.
+  RFC 2616 says this SHOULD NOT be more than 2, but everyone on the modern web
+   ignores that.
+  If it makes you feel better, we'll only ever be reading from one of these at
+   a time; the others are kept around mainly to avoid slow-starting a new
+   connection if we seek back near somewhere we were reading before.*/
+# define OP_NCONNS_MAX (4)
+
+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 LRU list (ordered from MRU to LRU) of connections.*/
+  OpusHTTPConn    *lru_head;
+  /*The free list.*/
+  OpusHTTPConn    *free_head;
+  /*The URL to connect to.*/
+  OpusParsedURL    url;
+  /*The connection we're currently reading from.
+    This can be -1 if no connection is active.*/
+  int              cur_conni;
+  /*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;
+  /*A buffer used to build HTTP requests.*/
+  OpusStringBuf    request;
+  /*The offset of the tail of the request.
+    Only the offset in the Range: header appears after this.*/
+  int              request_tail;
+  /*Whether or not the server supports range requests.*/
+  int              seekable;
+  /*The Content-Length, if specified, or -1 otherwise.
+    This will always be valid for seekable streams.*/
+  opus_int64       content_length;
+  /*The position indicator used when no connection is active.*/
+  opus_int64       pos;
+};
+
+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;
+  }
+  op_parsed_url_init(&_stream->url);
+  _stream->ssl_ctx=NULL;
+  op_sb_init(&_stream->request);
+  _stream->seekable=0;
+}
+
+/*Close the connection at the top of the LRU list.*/
+static void op_http_conn_close(OpusHTTPStream *_stream,OpusHTTPConn *_conn){
+  op_http_conn_clear(_conn);
+  _conn->ssl_conn=NULL;
+  _conn->fd=-1;
+  OP_ASSERT(_stream->lru_head==_conn);
+  _stream->lru_head=_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);
+  if(_stream->ssl_ctx!=NULL)SSL_CTX_free(_stream->ssl_ctx);
+  op_sb_clear(&_stream->request);
+  op_parsed_url_clear(&_stream->url);
+}
+
+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;
+  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(int _fd,int _nonblocking){
+  int flags;
+  flags=fcntl(_fd,F_GETFL);
+  if(OP_UNLIKELY(flags==-1))return flags;
+  if(_nonblocking)flags|=O_NONBLOCK;
+  else flags&=~O_NONBLOCK;
+  return fcntl(_fd,F_SETFL,flags);
+}
+
+/*Try to start a connection to the next address in the given list of a given
+   type.
+  _fd:         The socket to connect with.
+  [out] _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(int _fd,
+ struct addrinfo **_addr,int _ai_family){
+  struct addrinfo *addr;
+  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)!=-1)return 1;
+    if(OP_LIKELY(errno==EINPROGRESS))return 0;
+  }
+}
+
+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,int _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,-1)==-1)return OP_FALSE;
+  }
+}
+
+/*The number of address families to try connecting to simultaneously.*/
+# define OP_NPROTOS (2)
+
+static int op_http_connect(OpusHTTPStream *_stream,OpusHTTPConn *_conn,
+ struct addrinfo *_addrs){
+  struct addrinfo *addr;
+  struct addrinfo *addrs[OP_NPROTOS];
+  struct pollfd    fds[OP_NPROTOS];
+  int              ai_family;
+  int              nprotos;
+  int              ret;
+  int              pi;
+  int              pj;
+  for(pi=0;pi<OP_NPROTOS;pi++)addrs[pi]=NULL;
+  addr=_addrs;
+  /*Try connecting via both IPv4 and IPv6 simultaneously, and keep the first
+     one that succeeds.*/
+  for(;addr!=NULL;addr=addr->ai_next){
+    /*Give IPv6 a slight edge by putting it first in the list.*/
+    if(addr->ai_family==AF_INET6){
+      OP_ASSERT(addr->ai_addrlen<=sizeof(struct sockaddr_in6));
+      if(addrs[0]==NULL)addrs[0]=addr;
+    }
+    else if(addr->ai_family==AF_INET){
+      OP_ASSERT(addr->ai_addrlen<=sizeof(struct sockaddr_in));
+      if(addrs[1]==NULL)addrs[1]=addr;
+    }
+  }
+  /*Consolidate the list of addresses.*/
+  for(pi=nprotos=0;pi<OP_NPROTOS;pi++){
+    if(addrs[pi]!=NULL){
+      addrs[nprotos]=addrs[pi];
+      nprotos++;
+    }
+  }
+  /*Try to start a connection to each protocol.*/
+  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!=-1)){
+      if(OP_LIKELY(op_sock_set_nonblocking(fds[pi].fd,1)!=-1)){
+        ret=op_sock_connect_next(fds[pi].fd,addrs+pi,ai_family);
+        if(ret>1){
+          /*It succeeded right away, 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,-1)!=-1){
+    for(pi=0;pi<nprotos;pi++){
+      socklen_t errlen;
+      int       err;
+      /*Still waiting...*/
+      if(!fds[pi].revents)continue;
+      errlen=sizeof(err);
+      if(getsockopt(fds[pi].fd,SOL_SOCKET,SO_ERROR,&err,&errlen)!=-1&&err==0){
+        /*Success!*/
+        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;
+    BIO *ssl_bio;
+    /*Start the SSL connection.*/
+    OP_ASSERT(_stream->ssl_ctx!=NULL);
+    ssl_conn=SSL_new(_stream->ssl_ctx);
+    if(OP_LIKELY(ssl_conn!=NULL)){
+      ssl_bio=BIO_new_socket(fds[pi].fd,BIO_NOCLOSE);
+      if(OP_LIKELY(ssl_bio!=NULL)){
+        SSL_set_bio(ssl_conn,ssl_bio,ssl_bio);
+        SSL_set_connect_state(ssl_conn);
+        ret=op_do_ssl_step(ssl_conn,fds[pi].fd,SSL_connect);
+        if(OP_LIKELY(ret>0)){
+          ret=op_do_ssl_step(ssl_conn,fds[pi].fd,SSL_do_handshake);
+          if(OP_LIKELY(ret>0)){
+            _conn->ssl_conn=ssl_conn;
+            _conn->fd=fds[pi].fd;
+            return 0;
+          }
+        }
+        /*If we wanted to shut down cleanly, we would do:
+        op_do_ssl_step(ssl_conn,fds[pi].fd,SSL_shutdown);*/
+      }
+      SSL_free(ssl_conn);
+    }
+    close(fds[pi].fd);
+    return OP_FALSE;
+  }
+  /*Just a normal non-SSL connection.*/
+  _conn->ssl_conn=NULL;
+  _conn->fd=fds[pi].fd;
+  return 0;
+}
+
+# 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;
+  ret=op_sb_append(_sb,_user,user_len);
+  OP_ASSERT(!ret);
+  ret=op_sb_append(_sb,":",1);
+  OP_ASSERT(!ret);
+  ret=op_sb_append(_sb,_pass,pass_len);
+  OP_ASSERT(!ret);
+  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_conn_write_fully(OpusHTTPConn *_conn,
+ const char *_buf,int _size){
+  struct pollfd  fd;
+  SSL           *ssl_conn;
+  fd.fd=_conn->fd;
+  ssl_conn=_conn->ssl_conn;
+  while(_size>0){
+    int err;
+    if(ssl_conn!=NULL){
+      int ret;
+      ret=SSL_write(ssl_conn,_buf,_size);
+      if(ret>0){
+        /*Wrote some data.*/
+        _buf+=ret;
+        _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;
+      errno=0;
+      ret=write(fd.fd,_buf,_size);
+      if(ret>0){
+        _buf+=ret;
+        _size-=ret;
+        continue;
+      }
+      err=errno;
+      if(err!=EAGAIN&&err!=EWOULDBLOCK)return OP_FALSE;
+      fd.events=POLLOUT;
+    }
+    if(poll(&fd,1,-1)==-1)return OP_FALSE;
+  }
+  return 0;
+}
+
+/*Tries to read from the given connection.
+  [out] _buf: Returns the data read.
+  _size:      The size of the buffer.
+  _block      Whether or not to block until some data is retrieved.*/
+static ptrdiff_t op_http_conn_read(OpusHTTPConn *_conn,
+ char *_buf,ptrdiff_t _size,int _block){
+  struct pollfd   fd;
+  SSL            *ssl_conn;
+  ptrdiff_t       nread;
+  fd.fd=_conn->fd;
+  ssl_conn=_conn->ssl_conn;
+  nread=0;
+  do{
+    int err;
+    if(ssl_conn!=NULL){
+      int ret;
+      ret=SSL_read(ssl_conn,_buf+nread,_size-nread);
+      if(ret>0){
+        /*Read some data.
+          Keep going to see if there's more.*/
+        nread+=ret;
+        continue;
+      }
+      /*Connection closed.*/
+      else if(ret==0)break;
+      /*If we already read some data, return it right now.*/
+      if(nread>0)break;
+      err=SSL_get_error(ssl_conn,ret);
+      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;
+      else return 0;
+    }
+    else{
+      ssize_t ret;
+      errno=0;
+      ret=read(fd.fd,_buf+nread,_size-nread);
+      if(ret>0){
+        /*Read some data.
+          Keep going to see if there's more.*/
+        nread+=ret;
+        continue;
+      }
+      /*If we already read some data, return it right now.*/
+      if(nread>0)break;
+      err=errno;
+      if(err!=EAGAIN&&err!=EWOULDBLOCK)return 0;
+      fd.events=POLLIN;
+    }
+    if(!_block)break;
+    /*Need to wait to get any data at all.*/
+    if(poll(&fd,1,-1)==-1)return 0;
+  }
+  while(nread<_size);
+  return nread;
+}
+
+/*Reads the entirety of a response to an HTTP request into a buffer.
+  Actual parsing and validation is done later.*/
+static int op_http_conn_read_response(OpusHTTPConn *_conn,
+ char *_buf,int _size){
+  /*The remaining size of the buffer.*/
+  int size;
+  /*How many characters we've yet to see from the "\r\n\r\n" terminator.*/
+  int state;
+  size=_size;
+  state=4;
+  while(size>=state){
+    ptrdiff_t ret;
+    int       len;
+    ret=op_http_conn_read(_conn,_buf,state,1);
+    if(ret<=0)return OP_FALSE;
+    /*We read some data.*/
+    _buf+=ret;
+    size-=ret;
+    len=_size-size;
+    /*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(len-ret<4&&op_strncasecmp(_buf-len,"HTTP",OP_MIN(len,4))!=0){
+      return OP_FALSE;
+    }
+    /*How far along on the "\r\n\r\n" terminator are we?*/
+    if(*(_buf-1)=='\n'){
+      if(len>=2&&*(_buf-2)=='\r'){
+        if(len>=4&&*(_buf-3)=='\n'&&*(_buf-4)=='\r')return len;
+        state=2;
+      }
+      else state=4;
+    }
+    else if(*(_buf-1)=='\r'){
+      state=3;
+      if(len>=3&&*(_buf-2)=='\n'&&*(_buf-3)=='\r')state=1;
+    }
+    else state=4;
+  }
+  /*Not enough space left in the buffer to add the characters we'd need to get
+     a valid terminator.*/
+  return OP_EIMPL;
+}
+
+/*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 maximum size of a response message (before the body).
+  Responses larger than this will be discarded.*/
+# define OP_RESPONSE_SIZE_MAX (1024)
+
+# define OP_HTTP_DIGIT "01234567890"
+
+/*The Reason-Phrase is not allowed to contain control characters, except
+   horizontal tab (HT).*/
+# 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"
+
+/*These also include ' ' and '\t', but we get those from 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;
+    else if(_s[i]=='\t'||_s[i]==' ')i++;
+    else return i;
+  }
+}
+
+static char *op_http_parse_status_line(char **_status_code,char *_response){
+  char   *next;
+  char   *status_code;
+  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);
+  if(OP_UNLIKELY(d<=0))return NULL;
+  next+=d;
+  if(OP_UNLIKELY(*next++!='.'))return NULL;
+  d=strspn(next,OP_HTTP_DIGIT);
+  if(OP_UNLIKELY(d<=0))return NULL;
+  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);
+  if(OP_UNLIKELY(*next++!='\r'))return NULL;
+  if(OP_UNLIKELY(*next++!='\n'))return NULL;
+  *_status_code=status_code;
+  return next;
+}
+
+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;
+  if(next[0]=='\r'&&next[1]=='\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);
+  if(OP_UNLIKELY(*next++!='\r'))return OP_FALSE;
+  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  content_length;
+  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;
+  content_length=0;
+  for(i=0;i<next-_cdr;i++){
+    int digit;
+    digit=_cdr[i]-'0';
+    /*Check for overflow.*/
+    if(OP_UNLIKELY(content_length>(OP_INT64_MAX-9)/10+(digit<=7))){
+      return OP_EIMPL;
+    }
+    content_length=content_length*10+digit;
+  }
+  return content_length;
+}
+
+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!=-1&&OP_UNLIKELY(last>=length))return OP_FALSE;
+  *_first=first;
+  *_last=last;
+  *_length=length;
+  return 0;
+}
+
+static int op_http_stream_open(OpusHTTPStream *_stream,const char *_url,
+ int _flags,const char *_proxy_host,unsigned _proxy_port,
+ const char *_proxy_user,const char *_proxy_pass){
+  struct addrinfo *addrs;
+  const char      *last_host;
+  unsigned         last_port;
+  int              nredirs;
+  int              ret;
+  if(_proxy_host!=NULL&&OP_UNLIKELY(_proxy_port>65535U))return OP_EINVAL;
+  last_host=NULL;
+  ret=op_parse_url(&_stream->url,_url);
+  if(OP_UNLIKELY(ret<0))return ret;
+  for(nredirs=0;nredirs<OP_REDIRECT_LIMIT;nredirs++){
+    char        response[OP_RESPONSE_SIZE_MAX];
+    char       *next;
+    char       *status_code;
+    const char *host;
+    unsigned    port;
+    if(_proxy_host==NULL){
+      host=_stream->url.host;
+      port=_stream->url.port;
+    }
+    else{
+      host=_proxy_host;
+      port=_proxy_port;
+    }
+    /*If connecting to the same place as last time, don't re-resolve it.*/
+    addrs=NULL;
+    if(last_host!=NULL){
+      if(strcmp(last_host,host)==0&&last_port==port)addrs=&_stream->addr_info;
+      if(last_host!=_proxy_host)_ogg_free((void *)last_host);
+    }
+    last_host=host;
+    last_port=port;
+    /*Initialize the SSL library if necessary.*/
+    if(OP_URL_IS_SSL(&_stream->url)){
+      SSL_CTX *ssl_ctx;
+      /*We need to establish a CONNECT tunnel to handle https proxying.
+        This isn't supported yet.*/
+      if(_proxy_host!=NULL)return OP_EIMPL;
+      /*TODO: The documentation says this is not re-entrant.*/
+      SSL_library_init();
+      /*Needed to get SHA2 algorithms with old OpenSSL versions.*/
+      OpenSSL_add_ssl_algorithms();
+      ssl_ctx=SSL_CTX_new(SSLv23_client_method());
+      if(ssl_ctx==NULL)return OP_EFAULT;
+      if(!(_flags&OP_SSL_SKIP_CERTIFICATE_CHECK)){
+        SSL_CTX_set_verify(ssl_ctx,SSL_VERIFY_PEER,NULL);
+      }
+      _stream->ssl_ctx=ssl_ctx;
+    }
+    /*Pop connection 0 off the free list and put it on the LRU list.*/
+    OP_ASSERT(_stream->free_head==_stream->conns+0);
+    _stream->lru_head=_stream->conns+0;
+    _stream->free_head=_stream->conns[0].next;
+    _stream->conns[0].next=NULL;
+    /*Actually make the connection.*/
+    if(addrs!=&_stream->addr_info){
+      addrs=op_resolve(host,port);
+      if(OP_UNLIKELY(addrs==NULL))return OP_FALSE;
+    }
+    ret=op_http_connect(_stream,_stream->conns+0,addrs);
+    if(addrs!=&_stream->addr_info)freeaddrinfo(addrs);
+    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);
+    ret|=op_sb_append(&_stream->request," HTTP/1.0\r\n",11);
+    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)){
+      char port[7];
+      OP_ASSERT(_stream->url.port<=65535U);
+      sprintf(port,":%u",_stream->url.port);
+      ret|=op_sb_append_string(&_stream->request,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&&_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.*/
+    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,
+     response,sizeof(response)/sizeof(*response));
+    if(OP_UNLIKELY(ret<0))return ret;
+    next=op_http_parse_status_line(&status_code,response);
+    if(next==NULL)return OP_FALSE;
+    if(status_code[0]=='2'){
+      opus_int64 content_length;
+      opus_int64 range_length;
+      /*We only understand 20x codes.*/
+      if(status_code[1]!='0')return OP_FALSE;
+      content_length=-1;
+      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-length")==0){
+          /*Two Content-Length headers?*/
+          if(OP_UNLIKELY(content_length!=-1))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!=-1&&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!=-1))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 NOTE
+             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!=-1&&OP_UNLIKELY(content_length!=range_length)){
+            return OP_FALSE;
+          }
+        }
+      }
+      switch(status_code[2]){
+        /*200 OK*/
+        case '0':break;
+        /*203 Non-Authoritative Information*/
+        case '3':break;
+        /*204 No Content*/
+        case '4':{
+          if(content_length!=-1&&OP_UNLIKELY(content_length!=0)){
+            return OP_FALSE;
+          }
+        }break;
+        /*206 Partial Content*/
+        case '6':{
+          /*No Content-Range header.*/
+          if(OP_UNLIKELY(range_length==-1))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->conns[0].pos=0;
+      _stream->cur_conni=0;
+      /*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':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)
+        308...309 are not yet defined, so we don't know how to handle them.*/
+      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;
+    /*Don't free last_host if it came from the last URL.*/
+    if(last_host!=_proxy_host)_stream->url.host=NULL;
+    op_parsed_url_clear(&_stream->url);
+    ret=op_parse_url(&_stream->url,_url);
+    if(OP_UNLIKELY(ret<0)){
+      if(ret==OP_EINVAL)ret=OP_FALSE;
+      if(last_host!=_proxy_host)_ogg_free((void *)last_host);
+      return ret;
+    }
+  }
+  /*Redirection limit reached.*/
+  return OP_FALSE;
+}
+
+static int op_http_conn_open_pos(OpusHTTPStream *_stream,
+ OpusHTTPConn *_conn,opus_int64 _pos){
+  char        response[OP_RESPONSE_SIZE_MAX];
+  char       *next;
+  char       *status_code;
+  opus_int64  range_length;
+  int         ret;
+  ret=op_http_connect(_stream,_conn,&_stream->addr_info);
+  if(OP_UNLIKELY(ret<0))return ret;
+  /*Build the request to send.*/
+  _stream->request.nbuf=_stream->request_tail;
+  ret=op_sb_append_nonnegative_int64(&_stream->request,_pos);
+  ret|=op_sb_append(&_stream->request,"-\r\n\r\n",5);
+  if(OP_UNLIKELY(ret<0))return ret;
+  ret=op_http_conn_write_fully(_conn,
+   _stream->request.buf,_stream->request.nbuf);
+  if(OP_UNLIKELY(ret<0))return ret;
+  ret=op_http_conn_read_response(_conn,
+   response,sizeof(response)/sizeof(*response));
+  if(OP_UNLIKELY(ret<0))return ret;
+  next=op_http_parse_status_line(&status_code,response);
+  if(next==NULL)return OP_FALSE;
+  /*We _need_ a 206 Partial Content response.*/
+  if(strncmp(status_code,"206",3)!=0)return OP_FALSE;
+  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!=-1))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 NOTE
+         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;
+      /*Quit if we didn't get the offset we asked for.*/
+      if(range_first!=_pos)return OP_FALSE;
+      /*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-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;
+    }
+  }
+  /*No Content-Range header.*/
+  if(OP_UNLIKELY(range_length==-1))return OP_FALSE;
+  /*Update the content_length if necessary.*/
+  _stream->content_length=range_length;
+  _conn->pos=_pos;
+  _stream->cur_conni=_conn-_stream->conns;
+  OP_ASSERT(_stream->cur_conni>=0&&_stream->cur_conni<OP_NCONNS_MAX);
+  /*The connection has been successfully opened.*/
+  return 0;
+}
+
+static size_t op_http_stream_read(void *_ptr,size_t _size,size_t _nmemb,
+ void *_stream){
+  OpusHTTPStream *stream;
+  ptrdiff_t       nread;
+  ptrdiff_t       total;
+  opus_int64      size;
+  opus_int64      pos;
+  int             ci;
+  stream=(OpusHTTPStream *)_stream;
+  total=_size*_nmemb;
+  /*Check for overflow/empty read.*/
+  if(total==0||total/_size!=_nmemb||total>OP_INT64_MAX)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!=-1){
+    if(pos>=size)return 0;
+    /*Check for a short read.*/
+    if(total>size-pos){
+      _nmemb=(size-pos)/_size;
+      total=_size*_nmemb;
+    }
+  }
+  if(_size!=1){
+    ptrdiff_t n;
+    nread=0;
+    /*Read individual items one at a time.*/
+    do{
+      ptrdiff_t nread_item;
+      nread_item=0;
+      do{
+        /*Block on the first item, or if we've gotten a partial item.*/
+        n=op_http_conn_read(stream->conns+ci,
+         _ptr,_size-nread_item,nread==0||nread_item>0);
+        pos+=n;
+        nread_item+=n;
+      }
+      while(n>0&&nread_item<(ptrdiff_t)_size);
+      /*We can still fail to read a whole item if we encounter an error, or if
+         we hit EOF and didn't know the stream length.
+        TODO: The former is okay, the latter is not.*/
+      if(nread_item>=(ptrdiff_t)_size)nread++;
+      total-=_size;
+    }
+    while(n>0&&total>0);
+  }
+  else{
+    nread=op_http_conn_read(stream->conns+ci,_ptr,total,1);
+    pos+=nread;
+  }
+  if(OP_LIKELY(nread>0))stream->conns[ci].pos=pos;
+  else{
+    /*We either hit an error or EOF.
+      Either way, we're done with this connection.*/
+    op_http_conn_close(stream,stream->conns+ci);
+    stream->cur_conni=-1;
+    stream->pos=pos;
+  }
+  return nread;
+}
+
+/*To this will need to be larger than OP_CHUNK_SIZE to be useful.*/
+# define OP_READAHEAD_THRESH (128*1024)
+/*16 kB is the largest size OpenSSL will return at once.*/
+# define OP_READAHEAD_CHUNK_SIZE (16*1024)
+
+static int op_http_stream_seek(void *_stream,opus_int64 _offset,int _whence){
+  OpusHTTPStream  *stream;
+  OpusHTTPConn    *conn;
+  OpusHTTPConn    *prev;
+  OpusHTTPConn   **pnext;
+  OpusHTTPConn   **ppnext;
+  opus_int64       content_length;
+  opus_int64       pos;
+  int              ci;
+  int              ret;
+  stream=(OpusHTTPStream *)_stream;
+  if(!stream->seekable)return -1;
+  /*If we're seekable, we should have gotten a Content-Length.*/
+  content_length=stream->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;
+  }
+  /*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;
+  }
+  ppnext=NULL;
+  pnext=&stream->lru_head;
+  prev=NULL;
+  conn=stream->lru_head;
+  while(conn!=NULL){
+    opus_int64 conn_pos;
+    /*TODO: Estimate connection open time and current throughput, and compute
+       the read-ahead threshold accordingly.*/
+    /*TODO: Expire connections aggressively to avoid server timeouts.*/
+    conn_pos=conn->pos;
+    if(pos-OP_READAHEAD_THRESH<=conn_pos&&conn_pos<=pos){
+      /*Found a suitable connection to re-use.*/
+      *pnext=conn->next;
+      conn->next=stream->lru_head;
+      stream->lru_head=conn;
+      while(conn_pos<pos){
+        static char dummy_buf[OP_READAHEAD_CHUNK_SIZE];
+        ptrdiff_t nread;
+        nread=op_http_conn_read(conn,dummy_buf,
+         OP_MIN(pos-conn_pos,OP_READAHEAD_CHUNK_SIZE),1);
+        if(nread==0)break;
+        conn_pos+=nread;
+      }
+      conn->pos=conn_pos;
+      /*We failed to read ahead.*/
+      if(conn_pos<pos){
+        op_http_conn_close(stream,conn);
+        /*The connection might have become stale, so keep going.*/
+        conn=*pnext;
+        continue;
+      }
+      /*Sucessfully resurrected this connection.*/
+      stream->cur_conni=conn-stream->conns;
+      return 0;
+    }
+    ppnext=pnext;
+    pnext=&conn->next;
+    prev=conn;
+    conn=conn->next;
+  }
+  /*No suitable connections.
+    Open a new one.*/
+  if(stream->free_head==NULL){
+    /*All connections in use.
+      Expire the oldest one.*/
+    OP_ASSERT(prev!=NULL);
+    OP_ASSERT(ppnext!=NULL);
+    OP_ASSERT(prev->next==NULL);
+    *ppnext=NULL;
+    prev->next=stream->lru_head;
+    stream->lru_head=prev;
+    op_http_conn_close(stream,prev);
+  }
+  OP_ASSERT(stream->free_head!=NULL);
+  conn=stream->free_head;
+  stream->free_head=conn->next;
+  conn->next=stream->lru_head;
+  stream->lru_head=conn;
+  ret=op_http_conn_open_pos(stream,conn,pos);
+  if(OP_UNLIKELY(ret<0)){
+    op_http_conn_close(stream,conn);
+    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 *op_url_stream_create_with_proxy(OpusFileCallbacks *_cb,const char *_url,
+ int _flags,const char *_proxy_host,unsigned _proxy_port,
+ const char *_proxy_user,const char *_proxy_pass){
+  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(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(stream==NULL)return NULL;
+    op_http_stream_init(stream);
+    ret=op_http_stream_open(stream,_url,_flags,
+     _proxy_host,_proxy_port,_proxy_user,_proxy_pass);
+    if(OP_UNLIKELY(ret<0)){
+      op_http_stream_clear(stream);
+      _ogg_free(stream);
+      return NULL;
+    }
+    *_cb=*&OP_HTTP_CALLBACKS;
+    return stream;
+  }
+#else
+  _flags=_flags;
+  _proxy_host=_proxy_host;
+  _proxy_port=_proxy_port;
+  _proxy_user=_proxy_user;
+  _proxy_pass=_proxy_pass;
+  return NULL;
+#endif
+}
+
+void *op_url_stream_create(OpusFileCallbacks *_cb,const char *_url,int _flags){
+  return op_url_stream_create_with_proxy(_cb,_url,_flags,NULL,0,NULL,NULL);
+}
index 65f3973..b224fc5 100644 (file)
@@ -1259,6 +1259,20 @@ OggOpusFile *op_open_memory(const unsigned char *_data,size_t _size,
    _error);
 }
 
+OggOpusFile *op_open_url(const char *_url,int _flags,int *_error){
+  OpusFileCallbacks cb;
+  return op_open_close_on_failure(
+   op_url_stream_create(&cb,_url,_flags),&cb,_error);
+}
+
+OggOpusFile *op_open_url_with_proxy(const char *_url,int _flags,
+ const char *_proxy_host,unsigned _proxy_port,
+ const char *_proxy_user,const char *_proxy_pass,int *_error){
+  OpusFileCallbacks cb;
+  return op_open_close_on_failure(op_url_stream_create_with_proxy(&cb,_url,
+   _flags,_proxy_host,_proxy_port,_proxy_pass,_proxy_user),&cb,_error);
+}
+
 /*Convenience routine to clean up from failure for the open functions that
    create their own streams.*/
 static OggOpusFile *op_test_close_on_failure(void *_source,
@@ -1285,6 +1299,20 @@ OggOpusFile *op_test_memory(const unsigned char *_data,size_t _size,
    _error);
 }
 
+OggOpusFile *op_test_url(const char *_url,int _flags,int *_error){
+  OpusFileCallbacks cb;
+  return op_test_close_on_failure(
+   op_url_stream_create(&cb,_url,_flags),&cb,_error);
+}
+
+OggOpusFile *op_test_url_with_proxy(const char *_url,int _flags,
+ const char *_proxy_host,unsigned _proxy_port,
+ const char *_proxy_user,const char *_proxy_pass,int *_error){
+  OpusFileCallbacks cb;
+  return op_test_close_on_failure(op_url_stream_create_with_proxy(&cb,_url,
+   _flags,_proxy_host,_proxy_port,_proxy_pass,_proxy_user),&cb,_error);
+}
+
 int op_test_open(OggOpusFile *_of){
   int ret;
   if(OP_UNLIKELY(_of->ready_state!=OP_PARTOPEN))return OP_EINVAL;
index b0eafd3..a7ac939 100644 (file)
@@ -13,25 +13,36 @@ FOO_TARGET = foo
 # The command to use to generate dependency information
 MAKEDEPEND = ${CC} -MM
 #MAKEDEPEND = makedepend -f- -Y --
-# The location of include files.
-# Modify these to point to your Ogg and Vorbis include directories if they are
-#  not installed in a standard location.
-CINCLUDE = `pkg-config --cflags ogg opus`
-# These are gcc-only, but not actually critical.
+# Optional features to enable
+#CFLAGS := $(CFLAGS) -DOP_HAVE_LRINTF
+CFLAGS := $(CFLAGS) -DOP_ENABLE_HTTP
 # Extra compilation flags.
 # You may get speed increases by including flags such as -O2 or -O3 or
 #  -ffast-math, or additional flags, depending on your system and compiler.
 # The -g flag will generally include debugging information.
 CFLAGS := -g $(CFLAGS)
 CFLAGS := -DOP_ENABLE_ASSERTIONS $(CFLAGS)
+# These are gcc-only, but not actually critical.
 CFLAGS := -fPIC $(CFLAGS)
 CFLAGS := -std=c89 -pedantic $(CFLAGS)
 CFLAGS := -fvisibility=hidden $(CFLAGS)
 CFLAGS := -Wextra -Wno-parentheses -Wno-long-long $(CFLAGS)
 CFLAGS := -Wall $(CFLAGS)
+# The list of pkg-config packages we depend on.
+PACKAGES := ogg opus
+ifeq ($(findstring -DOP_ENABLE_HTTP,${CFLAGS}),-DOP_ENABLE_HTTP)
+PACKAGES += openssl
+endif
+# The location of include files.
+# Modify these to point to your Ogg and Vorbis include directories if they are
+#  not installed in a standard location.
+CINCLUDE := `pkg-config --cflags ${PACKAGES}`
 
 # Libraries to link with, and the location of library files.
-LIBS = `pkg-config --libs ogg opus`
+LIBS := `pkg-config --libs ${PACKAGES}`
+ifeq ($(findstring -DOP_HAVE_LRINTF,${CFLAGS}),-DOP_HAVE_LRINTF)
+LIBS := -lm $(LIBS)
+endif
 
 #TODO: tests
 FOO_LIBS =
@@ -45,6 +56,7 @@ WORKDIR = objs
 
 # C source file lists
 LIBOPUSFILE_CSOURCES = \
+http.c \
 info.c \
 internal.c \
 opusfile.c \