/*
 *      Copyright (C) 2014-2026 Jean-Luc Barriere
 *
 *  This library is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation; either version 3, or (at your option)
 *  any later version.
 *
 *  This library 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this library; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301 USA
 *  http://www.gnu.org/copyleft/gpl.html
 *
 */

#include "wsresponse.h"
#include "securesocket.h"
#include "compressor.h"
#include "debug.h"

#include <cstdlib>  // for atol
#include <cstdio>
#include <cstring>

#define HTTP_TOKEN_MAXLEN     79
#define HTTP_HEADER_MAXLEN    0x4000    // maximum header length (16k)
#define RESPONSE_BUFFER_SIZE  0x400     // size of read buffer for headers
#define RESPONSE_MAX_SIZE     0x20000   // maximum size for the entire response
#define CHUNK_MAX_SIZE        0x20000
#define REQUEST_BUFFER_SIZE   0x1000

using namespace NSROOT;

void WSResponse::init(const WSRequest &request, int maxRedirs, bool trustedLocation, bool followAny)
{
  p = new _response(request);
  while (0 < maxRedirs--)
  {
    int status = p->GetStatusCode();
    if (status == 301 || status == 302)
    {
      // handle redirection
      URIParser uri(p->Redirection());
      bool trusted = (uri.Scheme() && strncmp("https", uri.Scheme(), 5) == 0);
      unsigned port = uri.Port();
      if (!port)
        port = (trusted ? 443 : 80);
      bool samehost = (!uri.Host() || request.GetServer() == uri.Host());
      bool sameorigin = (!uri.Host() || (samehost && request.GetPort() == port));
      if (
          /* same origin  */ sameorigin ||
          /* same host    */ (samehost && (!trustedLocation || trusted)) ||
          /* follow any   */ (followAny && (!trustedLocation || trusted))
          )
      {
        DBG(DBG_DEBUG, "%s: (%d) LOCATION = %s\n", __FUNCTION__, p->GetStatusCode(), p->Redirection().c_str());
        WSRequest redir(request, uri);
        if (!sameorigin)
        {
          /* clear credentials */
          redir.ClearHeader(ws_header_to_upperstr(WS_HEADER_Authorization));
          redir.ClearHeader("COOKIE");
        }
        delete p;
        p = new _response(redir);
        continue;
      }
    }
    break;
  }
}

WSResponse::~WSResponse()
{
  if (p)
    delete p;
  p = nullptr;
}

bool WSResponse::ReadHeaderLine(NetSocket *socket, const char *eol, std::string& line, size_t *len)
{
  char buf[RESPONSE_BUFFER_SIZE];
  const char *s_eol;
  int p = 0, p_eol = 0, l_eol;
  size_t l = 0;

  if (eol != nullptr)
    s_eol = eol;
  else
    s_eol = "\n";
  l_eol = strlen(s_eol);

  line.clear();
  do
  {
    if (socket->ReceiveData(&buf[p], 1) > 0)
    {
      if (buf[p++] == s_eol[p_eol])
      {
        if (++p_eol >= l_eol)
        {
          buf[p - l_eol] = '\0';
          line.append(buf);
          l += p - l_eol;
          break;
        }
      }
      else
      {
        p_eol = 0;
        if (p > (RESPONSE_BUFFER_SIZE - 2 - l_eol))
        {
          buf[p] = '\0';
          line.append(buf);
          l += p;
          p = 0;
        }
      }
    }
    else
    {
      /* No EOL found until end of data */
      *len = l;
      return false;
    }
  }
  while (l < HTTP_HEADER_MAXLEN);

  *len = l;
  return true;
}

WSResponse::_response::_response(const WSRequest &request)
: m_socket(nullptr)
, m_successful(false)
, m_statusCode(0)
, m_serverInfo()
, m_etag()
, m_location()
, m_contentEncoding(WS_CENCODING_None)
, m_hasContent(false)
, m_contentEmpty(false)
, m_contentChunked(false)
, m_chunkNext(false)
, m_contentLength(0)
, m_consumed(0)
, m_chunkBuffer(nullptr)
, m_chunkPtr(nullptr)
, m_chunkEOR(nullptr)
, m_chunkEnd(nullptr)
, m_decoder(nullptr)
{
  if (request.IsSecureURI())
    m_socket = SSLSessionFactory::Instance().NewClientSocket();
  else
    m_socket = new TcpSocket();
  if (!m_socket)
    DBG(DBG_ERROR, "%s: create socket failed\n", __FUNCTION__);
  else if (m_socket->Connect(request.GetServer().c_str(), request.GetPort(), SOCKET_RCVBUF_MINSIZE))
  {
    m_socket->SetReadAttempt(6); // 60 sec to hang up
    if (!request.WriteMessage(*this))
      DBG(DBG_WARN, "%s: broken request\n", __FUNCTION__);
    if (ReadResponse())
    {
      if (m_statusCode < 200)
        DBG(DBG_WARN, "%s: status %d\n", __FUNCTION__, m_statusCode);
      else if (m_statusCode < 300)
        m_successful = true;
      else if (m_statusCode < 400)
        m_successful = false;
      else if (m_statusCode < 500)
        DBG(DBG_ERROR, "%s: bad request (%d)\n", __FUNCTION__, m_statusCode);
      else
        DBG(DBG_ERROR, "%s: server error (%d)\n", __FUNCTION__, m_statusCode);
    }
    else
      DBG(DBG_ERROR, "%s: invalid response\n", __FUNCTION__);
  }
}

WSResponse::_response::~_response()
{
  if (m_decoder)
    delete m_decoder;
  m_decoder = nullptr;
  if (m_chunkBuffer)
    delete [] m_chunkBuffer;
  m_chunkBuffer = m_chunkPtr = m_chunkEOR = m_chunkEnd = nullptr;
  if (m_socket)
    delete m_socket;
  m_socket = nullptr;
}

bool WSResponse::_response::WriteRequestStream(const char * data, unsigned len)
{
  if (!m_chunkBuffer)
  {
    if (!(m_chunkBuffer = new char[REQUEST_BUFFER_SIZE]))
      return false;
    m_chunkPtr = m_chunkEOR = m_chunkBuffer;
    m_chunkEnd = m_chunkBuffer + REQUEST_BUFFER_SIZE;
  }

  for(;;)
  {
    size_t s = m_chunkEnd - m_chunkPtr;
    if (s >= len )
    {
      memcpy(m_chunkPtr, data, len);
      m_chunkPtr += len;
      return true;
    }
    else
    {
      memcpy(m_chunkPtr, data, s);
      data += s;
      if (!m_socket->SendData(m_chunkBuffer, REQUEST_BUFFER_SIZE))
      {
        DBG(DBG_ERROR, "%s: failed (%d)\n", __FUNCTION__, m_socket->GetErrNo());
        delete [] m_chunkBuffer;
        m_chunkBuffer = m_chunkPtr = m_chunkEOR = m_chunkEnd = nullptr;
        return false;
      }
      m_chunkPtr = m_chunkBuffer;
      len -= s;
    }
  }
}

bool WSResponse::_response::FlushRequestStream()
{
  if (!m_chunkBuffer)
    return false;
  bool ret = true;
  size_t s = m_chunkPtr - m_chunkBuffer;
  if (s > 0 && !m_socket->SendData(m_chunkBuffer, s))
  {
    DBG(DBG_ERROR, "%s: failed (%d)\n", __FUNCTION__, m_socket->GetErrNo());
    ret = false;
  }
  delete [] m_chunkBuffer;
  m_chunkBuffer = m_chunkPtr = m_chunkEOR = m_chunkEnd = nullptr;
  return ret;
}

bool WSResponse::_response::ReadResponse()
{
  size_t len;
  size_t sum = 0;
  std::string strread;
  char token[HTTP_TOKEN_MAXLEN + 1];
  int n = 0, token_len = 0;
  bool ret = false;

  token[0] = 0;
  while (WSResponse::ReadHeaderLine(m_socket, WS_CRLF, strread, &len))
  {
    /* The response length shouldn't exceed the limit */
    sum += len;
    if (sum > RESPONSE_MAX_SIZE)
      return false;

    const char *line = strread.c_str(), *val = nullptr;
    int value_len = 0;

    DBG(DBG_PROTO, "%s: %s\n", __FUNCTION__, line);
    /*
     * The first line of a Response message is the Status-Line, consisting of
     * the protocol version followed by a numeric status code and its associated
     * textual phrase, with each element separated by SP characters.
     */
    if (++n == 1)
    {
      int status;
      if (len > 5 && 0 == memcmp(line, "HTTP", 4) && 1 == sscanf(line, "%*s %d", &status))
      {
        /* We have received a valid feedback */
        m_statusCode = status;
        ret = true;
      }
      else
      {
        /* Not a response header */
        return false;
      }
    }

    if (len == 0)
    {
      /* End of header */
      break;
    }

    /*
     * Header fields can be extended over multiple lines by preceding each
     * extra line with at least one SP or HT.
     */
    if ((line[0] == ' ' || line[0] == '\t') && token_len)
    {
      /* Append value of previous token */
      val = line;
    }
      /*
       * Each header field consists of a name followed by a colon (":") and the
       * field value. Field names are case-insensitive. The field value MAY be
       * preceded by any amount of LWS, though a single SP is preferred.
       */
    else if ((val = strchr(line, ':')))
    {
      int p;
      if ((token_len = val - line) > HTTP_TOKEN_MAXLEN)
        token_len = HTTP_TOKEN_MAXLEN;
      for (p = 0; p < token_len; ++p)
        token[p] = toupper(line[p]);
      token[token_len] = 0;
      value_len = len - (val - line + 1);
      while (value_len > 0 && (*(++val) == ' ' || *val == '\t')) --value_len;
      WSHeader& hv = m_headers[token];
      hv.SetName(line, token_len);
      hv.AddValue("");
    }
    else
    {
      /* Unknown syntax! Close previous token */
      token_len = 0;
      token[token_len] = 0;
    }

    if (token_len && val)
    {
      std::string& newval = m_headers[token].Back().append(val);
      switch (ws_header_from_upperstr(token))
      {
        case WS_HEADER_ETag:
          m_etag.assign(newval);
          break;
        case WS_HEADER_Server:
          m_serverInfo.assign(newval);
          break;
        case WS_HEADER_Location:
          m_location.assign(newval);
          break;
        case WS_HEADER_Content_Type:
          m_hasContent = true;
          break;
        case WS_HEADER_Content_Length:
        {
          int64_t num = atol(newval.c_str());
          if (num >= 0)
          {
            m_contentLength = (size_t)num;
            m_contentEmpty = (num == 0);
          }
          break;
        }
        case WS_HEADER_Content_Encoding:
          m_contentEncoding = ws_cencoding_from_str(newval.c_str());
          if (m_contentEncoding == WS_CENCODING_UNKNOWN)
            DBG(DBG_ERROR, "%s: unsupported content encoding (%s)\n", __FUNCTION__, newval.c_str());
          break;
        case WS_HEADER_Transfer_Encoding:
          if (newval.find("chunked") != std::string::npos)
          {
            m_contentChunked = true;
            m_chunkNext = true;
          }
          break;
        default:
          break;
      }
    }
  }

  return ret;
}

int WSResponse::_response::ReadChunk(void *buf, size_t buflen)
{
  int s = 0;
  if (m_contentChunked)
  {
    // no more pending byte in chunk buffer
    if (m_chunkPtr >= m_chunkEnd)
    {
      // process next chunk
      if (m_chunkBuffer)
        delete [] m_chunkBuffer;
      m_chunkBuffer = m_chunkPtr = m_chunkEOR = m_chunkEnd = nullptr;
      std::string strread;
      size_t len = 0;
      while (WSResponse::ReadHeaderLine(m_socket, WS_CRLF, strread, &len) && len == 0);
      DBG(DBG_PROTO, "%s: chunked data (%s)\n", __FUNCTION__, strread.c_str());
      std::string chunkStr("0x0");
      uint32_t chunkSize;
      if (strread.empty() || sscanf(chunkStr.append(strread.substr(0, strread.find(';'))).c_str(), "%x", &chunkSize) != 1)
        return (-1);
      if (chunkSize > 0)
      {
        // check chunk-size overflow
        if (chunkSize > CHUNK_MAX_SIZE)
        {
          DBG(DBG_ERROR, "%s: chunk-size overflow (req=%u) (max=%u)\n", __FUNCTION__, chunkSize, (unsigned)CHUNK_MAX_SIZE);
          return (-1);
        }
        if (!(m_chunkBuffer = new char[chunkSize]))
          return (-1);
        m_chunkPtr = m_chunkEOR = m_chunkBuffer;
        m_chunkEnd = m_chunkBuffer + chunkSize;
      }
      else
      {
        // read chunk trailers
        while (WSResponse::ReadHeaderLine(m_socket, WS_CRLF, strread, &len) && len != 0);
        return 0; // that's the end of chunks
      }
    }
    // fill chunk buffer
    if (m_chunkPtr >= m_chunkEOR)
    {
      // ask for new data to fill in the chunk buffer
      // fill at last read position and until to the end
      m_chunkEOR += m_socket->ReceiveData(m_chunkEOR, m_chunkEnd - m_chunkEOR);
    }
    if ((s = m_chunkEOR - m_chunkPtr) < 0)
      return (-1);
    if (buflen < (size_t)s)
      s = (int)buflen;
    memcpy(buf, m_chunkPtr, s);
    m_chunkPtr += s;
    m_consumed += s;
  }
  return s;
}

int WSResponse::_response::SocketStreamReader(void *hdl, void *buf, int sz)
{
  _response *resp = static_cast<_response*>(hdl);
  if (resp == nullptr)
    return 0;
  int s = 0;
  // let read on unknown length
  if (!resp->m_contentLength)
    s = (int)resp->m_socket->ReceiveData(buf, sz);
  else if (resp->m_contentLength > resp->m_consumed)
  {
    size_t len = resp->m_contentLength - resp->m_consumed;
    s = (int)resp->m_socket->ReceiveData(buf, len > (size_t)sz ? (size_t)sz : len);
  }
  if (s <= 0)
    resp->m_consumed = resp->m_contentLength;
  else
    resp->m_consumed += s;
  return s;
}

int WSResponse::_response::ChunkStreamReader(void *hdl, void *buf, int sz)
{
  _response *resp = static_cast<_response*>(hdl);
  if (resp && resp->m_chunkNext)
  {
    int s = resp->ReadChunk(buf, sz);
    if (s <= 0)
      resp->m_chunkNext = false;
    return s;
  }
  return 0;
}

int WSResponse::_response::ReadContent(char* buf, size_t buflen)
{
  if (!m_contentChunked)
  {
    if (m_contentEncoding == WS_CENCODING_None)
    {
      if (m_contentLength > m_consumed)
      {
        size_t len = m_contentLength - m_consumed;
        int s = (int)m_socket->ReceiveData(buf, len > buflen ? buflen : len);
        if (s <= 0)
          m_consumed = m_contentLength;
        else
          m_consumed += s;
        return s;
      }
      else if (!m_contentEmpty && m_hasContent)
      {
        // let read on unknown length
        int s = (int)m_socket->ReceiveData(buf, buflen);
        return s;
      }
    }
    else if (m_contentEncoding == WS_CENCODING_Gzip || m_contentEncoding == WS_CENCODING_Deflate)
    {
      int s = 0;
      if (m_decoder == nullptr)
        m_decoder = new Decompressor(&SocketStreamReader, this, (m_contentEncoding == WS_CENCODING_Gzip));
      if (m_decoder->HasOutputData())
        s = (int)m_decoder->ReadOutput(buf, buflen);
      if (s == 0 && !m_decoder->IsCompleted())
      {
        if (m_decoder->HasStreamError())
          DBG(DBG_ERROR, "%s: decoding failed: stream error\n", __FUNCTION__);
        else if (m_decoder->HasBufferError())
          DBG(DBG_ERROR, "%s: decoding failed: buffer error\n", __FUNCTION__);
        else
          DBG(DBG_ERROR, "%s: decoding failed\n", __FUNCTION__);
        return (-1);
      }
      return s;
    }
  }
  else
  {
    if (m_contentEncoding == WS_CENCODING_None)
    {
      if (m_chunkNext)
      {
        int s = ReadChunk(buf, buflen);
        if (s <= 0)
          m_chunkNext = false;
        return s;
      }
    }
    else if (m_contentEncoding == WS_CENCODING_Gzip || m_contentEncoding == WS_CENCODING_Deflate)
    {
      int s = 0;
      if (m_decoder == nullptr)
        m_decoder = new Decompressor(&ChunkStreamReader, this, (m_contentEncoding == WS_CENCODING_Gzip));
      if (m_decoder->HasOutputData())
        s = (int)m_decoder->ReadOutput(buf, buflen);
      if (s == 0 && !m_decoder->IsCompleted())
      {
        if (m_decoder->HasStreamError())
          DBG(DBG_ERROR, "%s: decoding failed: stream error\n", __FUNCTION__);
        else if (m_decoder->HasBufferError())
          DBG(DBG_ERROR, "%s: decoding failed: buffer error\n", __FUNCTION__);
        else
          DBG(DBG_ERROR, "%s: decoding failed\n", __FUNCTION__);
        return (-1);
      }
      return s;
    }
  }
  return 0;
}

bool WSResponse::_response::GetHeaderValue(const std::string& header, std::string& value)
{
  value.clear();
  VARS::const_iterator it = m_headers.find(header);
  if (it == m_headers.end())
    return false;
  value.assign(it->second.Last());
  return true;
}

const std::string& WSResponse::_response::GetHeaderValue(const std::string& header) const
{
  static std::string emptyStr = "";
  VARS::const_iterator it = m_headers.find(header);
  if (it != m_headers.end())
    return it->second.Last();
  return emptyStr;
}
