//
// ZoneMinder Remote Camera Class Implementation, $Date$, $Revision$
// Copyright (C) 2001-2008 Philip Coombes
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
// 

#include "zm_remote_camera_http.h"
#include "zm_rtsp_auth.h"

#include "zm_mem_utils.h"

#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netdb.h>

#ifdef SOLARIS
#include <sys/filio.h> // FIONREAD and friends
#endif

RemoteCameraHttp::RemoteCameraHttp( int p_id, const std::string &p_method, const std::string &p_host, const std::string &p_port, const std::string &p_path, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture ) :
  RemoteCamera( p_id, "http", p_host, p_port, p_path, p_width, p_height, p_colours, p_brightness, p_contrast, p_hue, p_colour, p_capture )
{
  sd = -1;

  timeout.tv_sec = 0;
  timeout.tv_usec = 0;

  if ( p_method == "simple" )
    method = SIMPLE;
  else if ( p_method == "regexp" )
    method = REGEXP;
  else
    Fatal( "Unrecognised method '%s' when creating HTTP camera %d", p_method.c_str(), id );
  if ( capture )
  {
    Initialise();
  }
}

RemoteCameraHttp::~RemoteCameraHttp()
{
  if ( capture )
  {
    Terminate();
  }
}

void RemoteCameraHttp::Initialise()
{
  RemoteCamera::Initialise();

  if ( request.empty() )
  {
    request = stringtf( "GET %s HTTP/%s\r\n", path.c_str(), config.http_version );
    request += stringtf( "User-Agent: %s/%s\r\n", config.http_ua, ZM_VERSION );
    request += stringtf( "Host: %s\r\n", host.c_str());
    if ( strcmp( config.http_version, "1.0" ) == 0 )
      request += stringtf( "Connection: Keep-Alive\r\n" );
    if ( !auth.empty() )
      request += stringtf( "Authorization: Basic %s\r\n", auth64.c_str() );
    request += "\r\n";
    Debug( 2, "Request: %s", request.c_str() );
  }

  if ( !timeout.tv_sec )
  {
    timeout.tv_sec = config.http_timeout/1000; 
    timeout.tv_usec = (config.http_timeout%1000)*1000;
  }

  int max_size = width*height*colours;

  buffer.size( max_size );

  mode = SINGLE_IMAGE;
  format = UNDEF;
  state = HEADER;
}

int RemoteCameraHttp::Connect()
{
  struct addrinfo *p;

  for(p = hp; p != NULL; p = p->ai_next)
  {
    sd = socket( p->ai_family, p->ai_socktype, p->ai_protocol );
    if ( sd < 0 )
    {
      Warning("Can't create socket: %s", strerror(errno) );
      continue;
    }

    if ( connect( sd, p->ai_addr, p->ai_addrlen ) < 0 )
    {
      close(sd);
      sd = -1;
      Warning("Can't connect to remote camera: %s", strerror(errno) );
      continue;
    }

  /* If we got here, we must have connected successfully */
  break;
  }

  if(p == NULL) {
    Error("Unable to connect to the remote camera, aborting");
    return( -1 );
  }

  Debug( 3, "Connected to host, socket = %d", sd );
  return( sd );
}

int RemoteCameraHttp::Disconnect()
{
  close( sd );
  sd = -1;
  Debug( 3, "Disconnected from host" );
  return( 0 );
}

int RemoteCameraHttp::SendRequest()
{
  Debug( 2, "Sending request: %s", request.c_str() );
  if ( write( sd, request.data(), request.length() ) < 0 )
  {
    Error( "Can't write: %s", strerror(errno) );
    Disconnect();
    return( -1 );
  }
  format = UNDEF;
  state = HEADER;
  Debug( 3, "Request sent" );
  return( 0 );
}

/* Return codes are as follows:
 * -1 means there was an error
 * 0 means no bytes were returned but there wasn't actually an error.
 * > 0 is the # of bytes read.
 */

int RemoteCameraHttp::ReadData( Buffer &buffer, int bytes_expected )
{
  fd_set rfds;
  FD_ZERO(&rfds);
  FD_SET(sd, &rfds);

  struct timeval temp_timeout = timeout;

  int n_found = select( sd+1, &rfds, NULL, NULL, &temp_timeout );
  if( n_found == 0 )
  {
    Debug( 4, "Select timed out timeout was %d secs %d usecs", temp_timeout.tv_sec, temp_timeout.tv_usec );
    // Why are we disconnecting?  It's just a timeout, meaning that data wasn't available.
    //Disconnect();
    return( 0 );
  }
  else if ( n_found < 0)
  {
    Error( "Select error: %s", strerror(errno) );
    return( -1 );
  }

  int total_bytes_to_read = 0;

  if ( bytes_expected )
  {
    total_bytes_to_read = bytes_expected;
  }
  else
  {
    if ( ioctl( sd, FIONREAD, &total_bytes_to_read ) < 0 )
    {
      Error( "Can't ioctl(): %s", strerror(errno) );
      return( -1 );
    }

    if ( total_bytes_to_read == 0 )
    {
      // If socket is closed locally, then select will fail, but if it is closed remotely
      // then we have an exception on our socket.. but no data.
      Debug( 3, "Socket closed remotely" );
      //Disconnect(); // Disconnect is done outside of ReadData now.
      return( -1 );
    }

    // There can be lots of bytes available.  I've seen 4MB or more. This will vastly inflate our buffer size unnecessarily.
    if ( total_bytes_to_read > ZM_NETWORK_BUFSIZ ) {
      total_bytes_to_read = ZM_NETWORK_BUFSIZ;
      Debug(3, "Just getting 32K" );
    } else {
      Debug(3, "Just getting %d", total_bytes_to_read );
    }
  }
  Debug( 3, "Expecting %d bytes", total_bytes_to_read );

  int total_bytes_read = 0;
  do
  {
    int bytes_read = buffer.read_into( sd, total_bytes_to_read );
    if ( bytes_read < 0)
    {
      Error( "Read error: %s", strerror(errno) );
      return( -1 );
    }
    else if ( bytes_read == 0)
    {
      Debug( 2, "Socket closed" );
      //Disconnect(); // Disconnect is done outside of ReadData now.
      return( -1 );
    }
    else if ( bytes_read < total_bytes_to_read )
    {
      Error( "Incomplete read, expected %d, got %d", total_bytes_to_read, bytes_read );
      return( -1 );
    }
    Debug( 3, "Read %d bytes", bytes_read );
    total_bytes_read += bytes_read;
    total_bytes_to_read -= bytes_read;
  }
  while ( total_bytes_to_read );

  Debug( 4, buffer );

  return( total_bytes_read );
}

int RemoteCameraHttp::GetResponse()
{
  int buffer_len;
#if HAVE_LIBPCRE
  if ( method == REGEXP )
  {
    const char *header = 0;
    int header_len = 0;
    const char *http_version = 0;
    int status_code = 0;
    const char *status_mesg = 0;
    const char *connection_type = "";
    int content_length = 0;
    const char *content_type = "";
    const char *content_boundary = "";
    const char *subheader = 0;
    int subheader_len = 0;
    //int subcontent_length = 0;
    //const char *subcontent_type = "";

    while ( true )
    {
      switch( state )
      {
        case HEADER :
        {
          static RegExpr *header_expr = 0;
          static RegExpr *status_expr = 0;
          static RegExpr *connection_expr = 0;
          static RegExpr *content_length_expr = 0;
          static RegExpr *content_type_expr = 0;

          while ( ! ( buffer_len = ReadData( buffer ) ) ) {
          }
            if ( buffer_len < 0 ) {
              Error( "Unable to read header data" );
              return( -1 );
            }
          if ( !header_expr )
            header_expr = new RegExpr( "^(.+?\r?\n\r?\n)", PCRE_DOTALL );
          if ( header_expr->Match( (char*)buffer, buffer.size() ) == 2 )
          {
            header = header_expr->MatchString( 1 );
            header_len = header_expr->MatchLength( 1 );
            Debug( 4, "Captured header (%d bytes):\n'%s'", header_len, header );

            if ( !status_expr )
              status_expr = new RegExpr( "^HTTP/(1\\.[01]) +([0-9]+) +(.+?)\r?\n", PCRE_CASELESS );
            if ( status_expr->Match( header, header_len ) < 4 )
            {
              Error( "Unable to extract HTTP status from header" );
              return( -1 );
            }
            http_version = status_expr->MatchString( 1 );
            status_code = atoi( status_expr->MatchString( 2 ) );
            status_mesg = status_expr->MatchString( 3 );

            if ( status_code == 401 ) {
              if ( mNeedAuth ) {
                Error( "Failed authentication: " );
                return( -1 );
              }
              mNeedAuth = true;
              std::string Header = header;
        
              mAuthenticator->checkAuthResponse(Header);
              if ( mAuthenticator->auth_method() == zm::AUTH_DIGEST ) {
                Debug( 2, "Need Digest Authentication" );
                request = stringtf( "GET %s HTTP/%s\r\n", path.c_str(), config.http_version );
                request += stringtf( "User-Agent: %s/%s\r\n", config.http_ua, ZM_VERSION );
                request += stringtf( "Host: %s\r\n", host.c_str());
                if ( strcmp( config.http_version, "1.0" ) == 0 )
                  request += stringtf( "Connection: Keep-Alive\r\n" );
                request += mAuthenticator->getAuthHeader( "GET", path.c_str() );
                request += "\r\n";
                
                Debug( 2, "New request header: %s", request.c_str() );
                return( 0 );
              } 

            } else if ( status_code < 200 || status_code > 299 ) {
              Error( "Invalid response status %d: %s\n%s", status_code, status_mesg, (char *)buffer );
              return( -1 );
            }
            Debug( 3, "Got status '%d' (%s), http version %s", status_code, status_mesg, http_version );

            if ( !connection_expr )
              connection_expr = new RegExpr( "Connection: ?(.+?)\r?\n", PCRE_CASELESS );
            if ( connection_expr->Match( header, header_len ) == 2 )
            {
              connection_type = connection_expr->MatchString( 1 );
              Debug( 3, "Got connection '%s'", connection_type );
            }

            if ( !content_length_expr )
              content_length_expr = new RegExpr( "Content-length: ?([0-9]+)\r?\n", PCRE_CASELESS );
            if ( content_length_expr->Match( header, header_len ) == 2 )
            {
              content_length = atoi( content_length_expr->MatchString( 1 ) );
              Debug( 3, "Got content length '%d'", content_length );
            }

            if ( !content_type_expr )
              content_type_expr = new RegExpr( "Content-type: ?(.+?)(?:; ?boundary=\x22?(.+?)\x22?)?\r?\n", PCRE_CASELESS );
            if ( content_type_expr->Match( header, header_len ) >= 2 )
            {
              content_type = content_type_expr->MatchString( 1 );
              Debug( 3, "Got content type '%s'\n", content_type );
              if ( content_type_expr->MatchCount() > 2 )
              {
                content_boundary = content_type_expr->MatchString( 2 );
                Debug( 3, "Got content boundary '%s'", content_boundary );
              }
            }

            if ( !strcasecmp( content_type, "image/jpeg" ) || !strcasecmp( content_type, "image/jpg" ) )
            {
              // Single image
              mode = SINGLE_IMAGE;
              format = JPEG;
              state = CONTENT;
            }
            else if ( !strcasecmp( content_type, "image/x-rgb" ) )
            {
              // Single image
              mode = SINGLE_IMAGE;
              format = X_RGB;
              state = CONTENT;
            }
            else if ( !strcasecmp( content_type, "image/x-rgbz" ) )
            {
              // Single image
              mode = SINGLE_IMAGE;
              format = X_RGBZ;
              state = CONTENT;
            }
            else if ( !strcasecmp( content_type, "multipart/x-mixed-replace" ) )
            {
              // Image stream, so start processing
              if ( !content_boundary[0] )
              {
                Error( "No content boundary found in header '%s'", header );
                return( -1 );
              }
              mode = MULTI_IMAGE;
              state = SUBHEADER;
            }
            //else if ( !strcasecmp( content_type, "video/mpeg" ) || !strcasecmp( content_type, "video/mpg" ) )
            //{
              //// MPEG stream, coming soon!
            //}
            else
            {
              Error( "Unrecognised content type '%s'", content_type );
              return( -1 );
            }
            buffer.consume( header_len );
          }
          else
          {
            Debug( 3, "Unable to extract header from stream, retrying" );
            //return( -1 );
          }
          break;
        }
        case SUBHEADER :
        {
          static RegExpr *subheader_expr = 0;
          static RegExpr *subcontent_length_expr = 0;
          static RegExpr *subcontent_type_expr = 0;

          if ( !subheader_expr )
          {
            char subheader_pattern[256] = "";
            snprintf( subheader_pattern, sizeof(subheader_pattern), "^((?:\r?\n){0,2}?(?:--)?%s\r?\n.+?\r?\n\r?\n)", content_boundary );
            subheader_expr = new RegExpr( subheader_pattern, PCRE_DOTALL );
          }
          if ( subheader_expr->Match( (char *)buffer, (int)buffer ) == 2 )
          {
            subheader = subheader_expr->MatchString( 1 );
            subheader_len = subheader_expr->MatchLength( 1 );
            Debug( 4, "Captured subheader (%d bytes):'%s'", subheader_len, subheader );

            if ( !subcontent_length_expr )
              subcontent_length_expr = new RegExpr( "Content-length: ?([0-9]+)\r?\n", PCRE_CASELESS );
            if ( subcontent_length_expr->Match( subheader, subheader_len ) == 2 )
            {
              content_length = atoi( subcontent_length_expr->MatchString( 1 ) );
              Debug( 3, "Got subcontent length '%d'", content_length );
            }

            if ( !subcontent_type_expr )
              subcontent_type_expr = new RegExpr( "Content-type: ?(.+?)\r?\n", PCRE_CASELESS );
            if ( subcontent_type_expr->Match( subheader, subheader_len ) == 2 )
            {
              content_type = subcontent_type_expr->MatchString( 1 );
              Debug( 3, "Got subcontent type '%s'", content_type );
            }

            buffer.consume( subheader_len );
            state = CONTENT;
          }
          else
          {
            Debug( 3, "Unable to extract subheader from stream, retrying" );
            while ( ! ( buffer_len = ReadData( buffer ) ) ) {
            }
              if ( buffer_len < 0 ) {
                Error( "Unable to extract subheader data" );
                return( -1 );
              }
          }
          break;
        }
        case CONTENT :
        {

          // if content_type is something like image/jpeg;size=, this will strip the ;size=
          char * semicolon = strchr( (char *)content_type, ';' );
          if ( semicolon ) {
            *semicolon = '\0';
          }

          if ( !strcasecmp( content_type, "image/jpeg" ) || !strcasecmp( content_type, "image/jpg" ) )
          {
            format = JPEG;
          }
          else if ( !strcasecmp( content_type, "image/x-rgb" ) )
          {
            format = X_RGB;
          }
          else if ( !strcasecmp( content_type, "image/x-rgbz" ) )
          {
            format = X_RGBZ;
          }
          else
          {
            Error( "Found unsupported content type '%s'", content_type );
            return( -1 );
          }

          if ( content_length )
          {
            while ( (long)buffer.size() < content_length )
            {
Debug(3, "Need more data buffer %d < content length %d", buffer.size(), content_length );
              if ( ReadData( buffer ) < 0 ) {
                Error( "Unable to read content" );
                return( -1 );
              }
            }
            Debug( 3, "Got end of image by length, content-length = %d", content_length );
          }
          else
          {
            while ( !content_length )
            {
              while ( ! ( buffer_len = ReadData( buffer ) ) ) {
              }
                if ( buffer_len < 0 ) {
                  Error( "Unable to read content" );
                  return( -1 );
                }
              static RegExpr *content_expr = 0;
              if ( mode == MULTI_IMAGE )
              {
                if ( !content_expr )
                {
                  char content_pattern[256] = "";
                  snprintf( content_pattern, sizeof(content_pattern), "^(.+?)(?:\r?\n)*(?:--)?%s\r?\n", content_boundary );
                  content_expr = new RegExpr( content_pattern, PCRE_DOTALL );
                }
                if ( content_expr->Match( buffer, buffer.size() ) == 2 )
                {
                  content_length = content_expr->MatchLength( 1 );
                  Debug( 3, "Got end of image by pattern, content-length = %d", content_length );
                }
              }
            }
          }
          if ( mode == SINGLE_IMAGE )
          {
            state = HEADER;
            Disconnect();
          }
          else
          {
            state = SUBHEADER;
          }
          Debug( 3, "Returning %d (%d) bytes of captured content", content_length, buffer.size() );
          return( content_length );
        }
        case HEADERCONT :
        case SUBHEADERCONT :
        {
          // Ignore
          break;
        }
      }
    }
  }
  else
#endif // HAVE_LIBPCRE
  {
    if ( method == REGEXP )
    {
      Warning( "Unable to use netcam regexps as not compiled with libpcre" );
    }
    static const char *http_match = "HTTP/";
    static const char *connection_match = "Connection:";
    static const char *content_length_match = "Content-length:";
    static const char *content_type_match = "Content-type:";
    static const char *boundary_match = "boundary=";
    static const char *authenticate_match = "WWW-Authenticate:";
    static int http_match_len = 0;
    static int connection_match_len = 0;
    static int content_length_match_len = 0;
    static int content_type_match_len = 0;
    static int boundary_match_len = 0;
    static int authenticate_match_len = 0;

    if ( !http_match_len )
      http_match_len = strlen( http_match );
    if ( !connection_match_len )
      connection_match_len = strlen( connection_match );
    if ( !content_length_match_len )
      content_length_match_len = strlen( content_length_match );
    if ( !content_type_match_len )
      content_type_match_len = strlen( content_type_match );
    if ( !boundary_match_len )
      boundary_match_len = strlen( boundary_match );
    if ( !authenticate_match_len )
      authenticate_match_len = strlen( authenticate_match );

    static int n_headers;
    //static char *headers[32];

    static int n_subheaders;
    //static char *subheaders[32];

    static char *http_header;
    static char *connection_header;
    static char *content_length_header;
    static char *content_type_header;
    static char *boundary_header;
    static char *authenticate_header;
    static char subcontent_length_header[32];
    static char subcontent_type_header[64];
  
    static char http_version[16];
    static char status_code[16];
    static char status_mesg[256];
    static char connection_type[32];
    static int content_length;
    static char content_type[32];
    static char content_boundary[64];
    static int content_boundary_len;

    while ( true )
    {
      switch( state )
      {
        case HEADER :
        {
          n_headers = 0;
          http_header = 0;
          connection_header = 0;
          content_length_header = 0;
          content_type_header = 0;
          authenticate_header = 0;

          http_version[0] = '\0';
          status_code [0]= '\0';
          status_mesg [0]= '\0';
          connection_type [0]= '\0';
          content_length = 0;
          content_type[0] = '\0';
          content_boundary[0] = '\0';
          content_boundary_len = 0;
        }
        case HEADERCONT :
        {
          while ( ! ( buffer_len = ReadData( buffer ) ) ) {
          }
            if ( buffer_len < 0 ) {
              Error( "Unable to read header" );
              return( -1 );
            }

          char *crlf = 0;
          char *header_ptr = (char *)buffer;
          int header_len = buffer.size();
          bool all_headers = false;

          while( true )
          {
            int crlf_len = memspn( header_ptr, "\r\n", header_len );
            if ( n_headers )
            {
              if ( (crlf_len == 2 && !strncmp( header_ptr, "\n\n", crlf_len )) || (crlf_len == 4 && !strncmp( header_ptr, "\r\n\r\n", crlf_len )) )
              {
                *header_ptr = '\0';
                header_ptr += crlf_len;
                header_len -= buffer.consume( header_ptr-(char *)buffer );
                all_headers = true;
                break;
              }
            }
            if ( crlf_len )
            {
              if ( header_len == crlf_len )
              {
                break;
              }
              else
              {
                *header_ptr = '\0';
                header_ptr += crlf_len;
                header_len -= buffer.consume( header_ptr-(char *)buffer );
              }
            }

            Debug( 6, "%s", header_ptr );
            if ( (crlf = mempbrk( header_ptr, "\r\n", header_len )) )
            {
              //headers[n_headers++] = header_ptr;
              n_headers++;

              if ( !http_header && (strncasecmp( header_ptr, http_match, http_match_len ) == 0) )
              {
                http_header = header_ptr+http_match_len;
                Debug( 6, "Got http header '%s'", header_ptr );
              }
              else if ( !connection_header && (strncasecmp( header_ptr, connection_match, connection_match_len) == 0) )
              {
                connection_header = header_ptr+connection_match_len;
                Debug( 6, "Got connection header '%s'", header_ptr );
              }
              else if ( !content_length_header && (strncasecmp( header_ptr, content_length_match, content_length_match_len) == 0) )
              {
                content_length_header = header_ptr+content_length_match_len;
                Debug( 6, "Got content length header '%s'", header_ptr );
              }

              else if ( !authenticate_header && (strncasecmp( header_ptr, authenticate_match, authenticate_match_len) == 0) )
              {
                authenticate_header = header_ptr;
                Debug( 6, "Got authenticate header '%s'", header_ptr );
              }
              else if ( !content_type_header && (strncasecmp( header_ptr, content_type_match, content_type_match_len) == 0) )
              {
                content_type_header = header_ptr+content_type_match_len;
                Debug( 6, "Got content type header '%s'", header_ptr );
              }
              else
              {
                Debug( 6, "Got ignored header '%s'", header_ptr );
              }
              header_ptr = crlf;
              header_len -= buffer.consume( header_ptr-(char *)buffer );
            }
            else
            {
              // No end of line found
              break;
            }
          }

          if ( all_headers )
          {
            char *start_ptr, *end_ptr;

            if ( !http_header )
            {
              Error( "Unable to extract HTTP status from header" );
              return( -1 );
            }

            start_ptr = http_header;
            end_ptr = start_ptr+strspn( start_ptr, "10." );

            memset( http_version, 0, sizeof(http_version) );
            strncpy( http_version, start_ptr, end_ptr-start_ptr );

            start_ptr = end_ptr;
            start_ptr += strspn( start_ptr, " " );
            end_ptr = start_ptr+strspn( start_ptr, "0123456789" );

            memset( status_code, 0, sizeof(status_code) );
            strncpy( status_code, start_ptr, end_ptr-start_ptr );
            int status = atoi( status_code );

            start_ptr = end_ptr;
            start_ptr += strspn( start_ptr, " " );
            strcpy( status_mesg, start_ptr );

            if ( status == 401 ) {
              if ( mNeedAuth ) {
                Error( "Failed authentication: " );
                return( -1 );
              }
              if ( ! authenticate_header ) {
                Error( "Failed authentication, but don't have an authentication header: " );
                return( -1 );
              }
              mNeedAuth = true;
              std::string Header = authenticate_header;
              Debug(2, "Checking for digest auth in %s", authenticate_header );

              mAuthenticator->checkAuthResponse(Header);
              if ( mAuthenticator->auth_method() == zm::AUTH_DIGEST ) {
                Debug( 2, "Need Digest Authentication" );
                request = stringtf( "GET %s HTTP/%s\r\n", path.c_str(), config.http_version );
                request += stringtf( "User-Agent: %s/%s\r\n", config.http_ua, ZM_VERSION );
                request += stringtf( "Host: %s\r\n", host.c_str());
                if ( strcmp( config.http_version, "1.0" ) == 0 )
                  request += stringtf( "Connection: Keep-Alive\r\n" );
                request += mAuthenticator->getAuthHeader( "GET", path.c_str() );
                request += "\r\n";

                Debug( 2, "New request header: %s", request.c_str() );
                return( 0 );
              } else {
                Debug( 2, "Need some other kind of Authentication" );
              }
            } else if ( status < 200 || status > 299 )
            {
              Error( "Invalid response status %s: %s", status_code, status_mesg );
              return( -1 );
            }
            Debug( 3, "Got status '%d' (%s), http version %s", status, status_mesg, http_version );

            if ( connection_header )
            {
              memset( connection_type, 0, sizeof(connection_type) );
              start_ptr = connection_header + strspn( connection_header, " " );
              strcpy( connection_type, start_ptr );
              Debug( 3, "Got connection '%s'", connection_type );
            }
            if ( content_length_header )
            {
              start_ptr = content_length_header + strspn( content_length_header, " " );
              content_length = atoi( start_ptr );
              Debug( 3, "Got content length '%d'", content_length );
            }
            if ( content_type_header )
            {
              memset( content_type, 0, sizeof(content_type) );
              start_ptr = content_type_header + strspn( content_type_header, " " );
              if ( (end_ptr = strchr( start_ptr, ';' )) )
              {
                strncpy( content_type, start_ptr, end_ptr-start_ptr );
                Debug( 3, "Got content type '%s'", content_type );

                start_ptr = end_ptr + strspn( end_ptr, "; " );

                if ( strncasecmp( start_ptr, boundary_match, boundary_match_len ) == 0 )
                {
                  start_ptr += boundary_match_len;
                  start_ptr += strspn( start_ptr, "-" );
                  content_boundary_len = sprintf( content_boundary, "--%s", start_ptr );
                  Debug( 3, "Got content boundary '%s'", content_boundary );
                }
                else
                {
                  Error( "No content boundary found in header '%s'", content_type_header );
                }
              }
              else
              {
                strcpy( content_type, start_ptr );
                Debug( 3, "Got content type '%s'", content_type );
              }
            }

            if ( !strcasecmp( content_type, "image/jpeg" ) || !strcasecmp( content_type, "image/jpg" ) )
            {
              // Single image
              mode = SINGLE_IMAGE;
              format = JPEG;
              state = CONTENT;
            }
            else if ( !strcasecmp( content_type, "image/x-rgb" ) )
            {
              // Single image
              mode = SINGLE_IMAGE;
              format = X_RGB;
              state = CONTENT;
            }
            else if ( !strcasecmp( content_type, "image/x-rgbz" ) )
            {
              // Single image
              mode = SINGLE_IMAGE;
              format = X_RGBZ;
              state = CONTENT;
            }
            else if ( !strcasecmp( content_type, "multipart/x-mixed-replace" ) )
            {
              // Image stream, so start processing
              if ( !content_boundary[0] )
              {
                Error( "No content boundary found in header '%s'", content_type_header );
                return( -1 );
              }
              mode = MULTI_IMAGE;
              state = SUBHEADER;
            }
            //else if ( !strcasecmp( content_type, "video/mpeg" ) || !strcasecmp( content_type, "video/mpg" ) )
            //{
              //// MPEG stream, coming soon!
            //}
            else
            {
              Error( "Unrecognised content type '%s'", content_type );
              return( -1 );
            }
          }
          else
          {
            Debug( 3, "Unable to extract entire header from stream, continuing" );
            state = HEADERCONT;
            //return( -1 );
          }
          break;
        }
        case SUBHEADER :
        {
          n_subheaders = 0;
          boundary_header = 0;
          subcontent_length_header[0] = '\0';
          subcontent_type_header[0] = '\0';
          content_length = 0;
          content_type[0] = '\0';
        }
        case SUBHEADERCONT :
        {
          char *crlf = 0;
          char *subheader_ptr = (char *)buffer;
          int subheader_len = buffer.size();
          bool all_headers = false;

          while( true )
          {
            int crlf_len = memspn( subheader_ptr, "\r\n", subheader_len );
            if ( n_subheaders )
            {
              if ( (crlf_len == 2 && !strncmp( subheader_ptr, "\n\n", crlf_len )) || (crlf_len == 4 && !strncmp( subheader_ptr, "\r\n\r\n", crlf_len )) )
              {
                *subheader_ptr = '\0';
                subheader_ptr += crlf_len;
                subheader_len -= buffer.consume( subheader_ptr-(char *)buffer );
                all_headers = true;
                break;
              }
            }
            if ( crlf_len )
            {
              if ( subheader_len == crlf_len )
              {
                break;
              }
              else
              {
                *subheader_ptr = '\0';
                subheader_ptr += crlf_len;
                subheader_len -= buffer.consume( subheader_ptr-(char *)buffer );
              }
            }

            Debug( 6, "%d: %s", subheader_len, subheader_ptr );

            if ( (crlf = mempbrk( subheader_ptr, "\r\n", subheader_len )) )
            {
              //subheaders[n_subheaders++] = subheader_ptr;
              n_subheaders++;

              if ( !boundary_header && (strncasecmp( subheader_ptr, content_boundary, content_boundary_len ) == 0) )
              {
                boundary_header = subheader_ptr;
                Debug( 4, "Got boundary subheader '%s'", subheader_ptr );
              }
              else if ( !subcontent_length_header[0] && (strncasecmp( subheader_ptr, content_length_match, content_length_match_len) == 0) )
              {
                strncpy( subcontent_length_header, subheader_ptr+content_length_match_len, sizeof(subcontent_length_header) );
                *(subcontent_length_header+strcspn( subcontent_length_header, "\r\n" )) = '\0';
                Debug( 4, "Got content length subheader '%s'", subcontent_length_header );
              }
              else if ( !subcontent_type_header[0] && (strncasecmp( subheader_ptr, content_type_match, content_type_match_len) == 0) )
              {
                strncpy( subcontent_type_header, subheader_ptr+content_type_match_len, sizeof(subcontent_type_header) );
                *(subcontent_type_header+strcspn( subcontent_type_header, "\r\n" )) = '\0';
                Debug( 4, "Got content type subheader '%s'", subcontent_type_header );
              }
              else
              {
                Debug( 6, "Got ignored subheader '%s' found", subheader_ptr );
              }
              subheader_ptr = crlf;
              subheader_len -= buffer.consume( subheader_ptr-(char *)buffer );
            }
            else
            {
              // No line end found
              break;
            }
          }
          
          if ( all_headers && boundary_header )
          {
            char *start_ptr/*, *end_ptr*/;

            Debug( 3, "Got boundary '%s'", boundary_header );

            if ( subcontent_length_header[0] )
            {
              start_ptr = subcontent_length_header + strspn( subcontent_length_header, " " );
              content_length = atoi( start_ptr );
              Debug( 3, "Got subcontent length '%d'", content_length );
            }
            if ( subcontent_type_header[0] )
            {
              memset( content_type, 0, sizeof(content_type) );
              start_ptr = subcontent_type_header + strspn( subcontent_type_header, " " );
              strcpy( content_type, start_ptr );
              Debug( 3, "Got subcontent type '%s'", content_type );
            }
            state = CONTENT;
          }
          else
          {
            Debug( 3, "Unable to extract subheader from stream, retrying" );
            while ( ! ( buffer_len = ReadData( buffer ) ) ) {
            }
              if ( buffer_len < 0 ) {
                Error( "Unable to read subheader" );
                return( -1 );
              }
            state = SUBHEADERCONT;
          }
          break;
        }
        case CONTENT :
        {

          // if content_type is something like image/jpeg;size=, this will strip the ;size=
          char * semicolon = strchr( content_type, ';' );
          if ( semicolon ) {
            *semicolon = '\0';
          }

          if ( !strcasecmp( content_type, "image/jpeg" ) || !strcasecmp( content_type, "image/jpg" ) )
          {
            format = JPEG;
          }
          else if ( !strcasecmp( content_type, "image/x-rgb" ) )
          {
            format = X_RGB;
          }
          else if ( !strcasecmp( content_type, "image/x-rgbz" ) )
          {
            format = X_RGBZ;
          }
          else
          {
            Error( "Found unsupported content type '%s'", content_type );
            return( -1 );
          }

          if ( format == JPEG && buffer.size() >= 2 )
          {
            if ( buffer[0] != 0xff || buffer[1] != 0xd8 )
            {
              Error( "Found bogus jpeg header '%02x%02x'", buffer[0], buffer[1] );
              return( -1 );
            }
          }

          if ( content_length )
          {
            while ( (long)buffer.size() < content_length )
            {
              //int buffer_len = ReadData( buffer, content_length-buffer.size() );
              if ( ReadData( buffer ) < 0 ) {
                Error( "Unable to read content" );
                return( -1 );
              }
            }
            Debug( 3, "Got end of image by length, content-length = %d", content_length );
          }
          else
          {
            int content_pos = 0;
            while ( !content_length )
            {
              buffer_len = ReadData( buffer );
              if ( buffer_len < 0 )
              {
                Error( "Unable to read content" );
                return( -1 );
              }
              int buffer_size = buffer.size();
              if ( buffer_len )
              {
                if ( mode == MULTI_IMAGE )
                {
                  while ( char *start_ptr = (char *)memstr( (char *)buffer+content_pos, "\r\n--", buffer_size-content_pos ) )
                  {
                    content_length = start_ptr - (char *)buffer;
                    Debug( 3, "Got end of image by pattern (crlf--), content-length = %d", content_length );
                    break;
                  }
                }
              }
              else
              {
                content_length = buffer_size;
                Debug( 3, "Got end of image by closure, content-length = %d", content_length );
                if ( mode == SINGLE_IMAGE )
                {
                  char *end_ptr = (char *)buffer+buffer_size;

                  while( *end_ptr == '\r' || *end_ptr == '\n' )
                  {
                    content_length--;
                    end_ptr--;
                  }

                  if ( end_ptr != ((char *)buffer+buffer_size) )
                  {
                    Debug( 3, "Trimmed end of image, new content-length = %d", content_length );
                  }
                }
              }
            }
          }
          if ( mode == SINGLE_IMAGE )
          {
            state = HEADER;
            Disconnect();
          }
          else
          {
            state = SUBHEADER;
          }

          if ( format == JPEG && buffer.size() >= 2 )
          {
            if ( buffer[0] != 0xff || buffer[1] != 0xd8 )
            {
              Error( "Found bogus jpeg header '%02x%02x'", buffer[0], buffer[1] );
              return( -1 );
            }
          }

          Debug( 3, "Returning %d bytes, buffer size: (%d) bytes of captured content", content_length, buffer.size() );
          return( content_length );
        }
      }
    }
  }
  return( 0 );
}

int RemoteCameraHttp::PreCapture()
{
  if ( sd < 0 )
  {
    Connect();
    if ( sd < 0 )
    {
      Error( "Unable to connect to camera" );
      return( -1 );
    }
    mode = SINGLE_IMAGE;
    buffer.clear();
  }
  if ( mode == SINGLE_IMAGE )
  {
    if ( SendRequest() < 0 )
    {
      Error( "Unable to send request" );
      Disconnect();
      return( -1 );
    }
  }
  return( 0 );
}

int RemoteCameraHttp::Capture( Image &image )
{
  int content_length = GetResponse();
  if ( content_length == 0 )
  {
    Warning( "Unable to capture image, retrying" );
    return( 1 );
  }
  if ( content_length < 0 )
  {
    Error( "Unable to get response, disconnecting" );
    Disconnect();
    return( -1 );
  }
  switch( format )
  {
    case JPEG :
    {
      if ( !image.DecodeJpeg( buffer.extract( content_length ), content_length, colours, subpixelorder ) )
      {
        Error( "Unable to decode jpeg" );
        Disconnect();
        return( -1 );
      }
      break;
    }
    case X_RGB :
    {
      if ( content_length != (long)image.Size() )
      {
        Error( "Image length mismatch, expected %d bytes, content length was %d", image.Size(), content_length );
        Disconnect();
        return( -1 );
      }
      image.Assign( width, height, colours, subpixelorder, buffer, imagesize );
      break;
    }
    case X_RGBZ :
    {
      if ( !image.Unzip( buffer.extract( content_length ), content_length ) )
      {
        Error( "Unable to unzip RGB image" );
        Disconnect();
        return( -1 );
      }
      image.Assign( width, height, colours, subpixelorder, buffer, imagesize );
      break;
    }
    default :
    {
      Error( "Unexpected image format encountered" );
      Disconnect();
      return( -1 );
    }
  }
  return( 0 );
}

int RemoteCameraHttp::PostCapture()
{
  return( 0 );
}
