/* Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "debugger.h"
#include "jcontext.h"
#include "jerryscript-port.h"

#ifdef JERRY_DEBUGGER

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

/**
 * Debugger socket communication port.
 */
#ifndef JERRY_DEBUGGER_PORT
#define JERRY_DEBUGGER_PORT 5001
#endif

/**
 * Masking-key is available.
 */
#define JERRY_DEBUGGER_WEBSOCKET_MASK_BIT 0x80

/**
 * Opcode type mask.
 */
#define JERRY_DEBUGGER_WEBSOCKET_OPCODE_MASK 0x0fu

/**
 * Packet length mask.
 */
#define JERRY_DEBUGGER_WEBSOCKET_LENGTH_MASK 0x7fu

/**
 * Payload mask size in bytes of a websocket package.
 */
#define JERRY_DEBUGGER_WEBSOCKET_MASK_SIZE 4

/**
 * Header for incoming packets.
 */
typedef struct
{
  uint8_t ws_opcode; /**< websocket opcode */
  uint8_t size; /**< size of the message */
  uint8_t mask[4]; /**< mask bytes */
} jerry_debugger_receive_header_t;

/**
 * Close the socket connection to the client.
 */
static void
jerry_debugger_close_connection_tcp (bool log_error) /**< log error */
{
  JERRY_ASSERT (JERRY_CONTEXT (debugger_flags) & JERRY_DEBUGGER_CONNECTED);

  JERRY_CONTEXT (debugger_flags) = (uint8_t) JERRY_DEBUGGER_VM_IGNORE;

  if (log_error)
  {
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
  }

  jerry_port_log (JERRY_LOG_LEVEL_DEBUG, "Debugger client connection closed.\n");

  close (JERRY_CONTEXT (debugger_connection));
  JERRY_CONTEXT (debugger_connection) = -1;

  jerry_debugger_free_unreferenced_byte_code ();
} /* jerry_debugger_close_connection_tcp */

/**
 * Send message to the client side.
 *
 * @return true - if the data was sent successfully to the client side
 *         false - otherwise
 */
static bool
jerry_debugger_send_tcp (const uint8_t *data_p, /**< data pointer */
                         size_t data_size) /**< data size */
{
  JERRY_ASSERT (JERRY_CONTEXT (debugger_flags) & JERRY_DEBUGGER_CONNECTED);

  do
  {
    ssize_t sent_bytes = send (JERRY_CONTEXT (debugger_connection), data_p, data_size, 0);

    if (sent_bytes < 0)
    {
      if (errno == EWOULDBLOCK)
      {
        continue;
      }

      jerry_debugger_close_connection_tcp (true);
      return false;
    }

    data_size -= (size_t) sent_bytes;
    data_p += sent_bytes;
  }
  while (data_size > 0);

  return true;
} /* jerry_debugger_send_tcp */

/**
 * Convert a 6-bit value to a Base64 character.
 *
 * @return Base64 character
 */
static uint8_t
jerry_to_base64_character (uint8_t value) /**< 6-bit value */
{
  if (value < 26)
  {
    return (uint8_t) (value + 'A');
  }

  if (value < 52)
  {
    return (uint8_t) (value - 26 + 'a');
  }

  if (value < 62)
  {
    return (uint8_t) (value - 52 + '0');
  }

  if (value == 62)
  {
    return (uint8_t) '+';
  }

  return (uint8_t) '/';
} /* jerry_to_base64_character */

/**
 * Encode a byte sequence into Base64 string.
 */
static void
jerry_to_base64 (const uint8_t *source_p, /**< source data */
                 uint8_t *destination_p, /**< destination buffer */
                 size_t length) /**< length of source, must be divisible by 3 */
{
  while (length >= 3)
  {
    uint8_t value = (source_p[0] >> 2);
    destination_p[0] = jerry_to_base64_character (value);

    value = (uint8_t) (((source_p[0] << 4) | (source_p[1] >> 4)) & 0x3f);
    destination_p[1] = jerry_to_base64_character (value);

    value = (uint8_t) (((source_p[1] << 2) | (source_p[2] >> 6)) & 0x3f);
    destination_p[2] = jerry_to_base64_character (value);

    value = (uint8_t) (source_p[2] & 0x3f);
    destination_p[3] = jerry_to_base64_character (value);

    source_p += 3;
    destination_p += 4;
    length -= 3;
  }
} /* jerry_to_base64 */

/**
 * Process WebSocket handshake.
 *
 * @return true - if the handshake was completed successfully
 *         false - otherwise
 */
static bool
jerry_process_handshake (int client_socket, /**< client socket */
                         uint8_t *request_buffer_p) /**< temporary buffer */
{
  size_t request_buffer_size = 1024;
  uint8_t *request_end_p = request_buffer_p;

  /* Buffer request text until the double newlines are received. */
  while (true)
  {
    size_t length = request_buffer_size - 1u - (size_t) (request_end_p - request_buffer_p);

    if (length == 0)
    {
      jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Handshake buffer too small.\n");
      return false;
    }

    ssize_t size = recv (client_socket, request_end_p, length, 0);

    if (size < 0)
    {
      jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
      return false;
    }

    request_end_p += (size_t) size;
    *request_end_p = 0;

    if (request_end_p > request_buffer_p + 4
        && memcmp (request_end_p - 4, "\r\n\r\n", 4) == 0)
    {
      break;
    }
  }

  /* Check protocol. */
  const char *text_p = "GET /jerry-debugger";
  size_t text_len = strlen (text_p);

  if ((size_t) (request_end_p - request_buffer_p) < text_len
      || memcmp (request_buffer_p, text_p, text_len) != 0)
  {
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Invalid handshake format.\n");
    return false;
  }

  uint8_t *websocket_key_p = request_buffer_p + text_len;

  text_p = "Sec-WebSocket-Key:";
  text_len = strlen (text_p);

  while (true)
  {
    if ((size_t) (request_end_p - websocket_key_p) < text_len)
    {
      jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Sec-WebSocket-Key not found.\n");
      return false;
    }

    if (websocket_key_p[0] == 'S'
        && websocket_key_p[-1] == '\n'
        && websocket_key_p[-2] == '\r'
        && memcmp (websocket_key_p, text_p, text_len) == 0)
    {
      websocket_key_p += text_len;
      break;
    }

    websocket_key_p++;
  }

  /* String terminated by double newlines. */

  while (*websocket_key_p == ' ')
  {
    websocket_key_p++;
  }

  uint8_t *websocket_key_end_p = websocket_key_p;

  while (*websocket_key_end_p > ' ')
  {
    websocket_key_end_p++;
  }

  /* Since the request_buffer_p is not needed anymore it can
   * be reused for storing the SHA-1 key and Base64 string. */

  const size_t sha1_length = 20;

  jerry_debugger_compute_sha1 (websocket_key_p,
                               (size_t) (websocket_key_end_p - websocket_key_p),
                               (const uint8_t *) "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
                               36,
                               request_buffer_p);

  /* The SHA-1 key is 20 bytes long but jerry_to_base64 expects
   * a length divisible by 3 so an extra 0 is appended at the end. */
  request_buffer_p[sha1_length] = 0;

  jerry_to_base64 (request_buffer_p, request_buffer_p + sha1_length + 1, sha1_length + 1);

  /* Last value must be replaced by equal sign. */

  text_p = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";

  if (!jerry_debugger_send_tcp ((const uint8_t *) text_p, strlen (text_p))
      || !jerry_debugger_send_tcp (request_buffer_p + sha1_length + 1, 27))
  {
    return false;
  }

  text_p = "=\r\n\r\n";
  return jerry_debugger_send_tcp ((const uint8_t *) text_p, strlen (text_p));
} /* jerry_process_handshake */

/**
 * Initialize the socket connection.
 *
 * @return true - if the connection succeeded
 *         false - otherwise
 */
bool
jerry_debugger_accept_connection (void)
{
  int server_socket;
  struct sockaddr_in addr;
  socklen_t sin_size = sizeof (struct sockaddr_in);

  JERRY_ASSERT (JERRY_CONTEXT (jerry_init_flags) & ECMA_INIT_DEBUGGER);

  addr.sin_family = AF_INET;
  addr.sin_port = htons (JERRY_DEBUGGER_PORT);
  addr.sin_addr.s_addr = INADDR_ANY;

  if ((server_socket = socket (AF_INET, SOCK_STREAM, 0)) == -1)
  {
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
    return false;
  }

  int opt_value = 1;

  if (setsockopt (server_socket, SOL_SOCKET, SO_REUSEADDR, &opt_value, sizeof (int)) == -1)
  {
    close (server_socket);
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
    return false;
  }

  if (bind (server_socket, (struct sockaddr *)&addr, sizeof (struct sockaddr)) == -1)
  {
    close (server_socket);
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
    return false;
  }

  if (listen (server_socket, 1) == -1)
  {
    close (server_socket);
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
    return false;
  }

  jerry_port_log (JERRY_LOG_LEVEL_DEBUG, "Waiting for client connection\n");

  JERRY_CONTEXT (debugger_connection) = accept (server_socket, (struct sockaddr *)&addr, &sin_size);

  if (JERRY_CONTEXT (debugger_connection) == -1)
  {
    close (server_socket);
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Error: %s\n", strerror (errno));
    return false;
  }

  close (server_socket);

  JERRY_CONTEXT (debugger_flags) = (uint8_t) (JERRY_CONTEXT (debugger_flags) | JERRY_DEBUGGER_CONNECTED);

  bool is_handshake_ok = false;

  JMEM_DEFINE_LOCAL_ARRAY (request_buffer_p, 1024, uint8_t);

  is_handshake_ok = jerry_process_handshake (JERRY_CONTEXT (debugger_connection),
                                             request_buffer_p);

  JMEM_FINALIZE_LOCAL_ARRAY (request_buffer_p);

  if (!is_handshake_ok)
  {
    jerry_debugger_close_connection ();
    return false;
  }

  if (!jerry_debugger_send_configuration (JERRY_DEBUGGER_MAX_RECEIVE_SIZE))
  {
    return false;
  }

  /* Set non-blocking mode. */
  int socket_flags = fcntl (JERRY_CONTEXT (debugger_connection), F_GETFL, 0);

  if (socket_flags < 0)
  {
    jerry_debugger_close_connection_tcp (true);
    return false;
  }

  if (fcntl (JERRY_CONTEXT (debugger_connection), F_SETFL, socket_flags | O_NONBLOCK) == -1)
  {
    jerry_debugger_close_connection_tcp (true);
    return false;
  }

  jerry_port_log (JERRY_LOG_LEVEL_DEBUG, "Connected from: %s\n", inet_ntoa (addr.sin_addr));

  JERRY_CONTEXT (debugger_flags) = (uint8_t) (JERRY_CONTEXT (debugger_flags) | JERRY_DEBUGGER_VM_STOP);
  JERRY_CONTEXT (debugger_stop_context) = NULL;

  return true;
} /* jerry_debugger_accept_connection */

/**
 * Close the socket connection to the client.
 */
inline void __attr_always_inline___
jerry_debugger_close_connection (void)
{
  jerry_debugger_close_connection_tcp (false);
} /* jerry_debugger_close_connection */

/**
 * Send message to the client side
 *
 * @return true - if the data was sent successfully to the debugger client,
 *         false - otherwise
 */
inline bool __attr_always_inline___
jerry_debugger_send (size_t data_size) /**< data size */
{
  return jerry_debugger_send_tcp (JERRY_CONTEXT (debugger_send_buffer), data_size);
} /* jerry_debugger_send */

JERRY_STATIC_ASSERT (JERRY_DEBUGGER_MAX_RECEIVE_SIZE < 126,
                     maximum_debug_message_receive_size_must_be_smaller_than_126);

/**
 * Receive message from the client.
 *
 * Note:
 *   If the function returns with true, the value of
 *   JERRY_DEBUGGER_VM_STOP flag should be ignored.
 *
 * @return true - if execution should be resumed,
 *         false - otherwise
 */
bool
jerry_debugger_receive (void)
{
  JERRY_ASSERT (JERRY_CONTEXT (debugger_flags) & JERRY_DEBUGGER_CONNECTED);

  JERRY_CONTEXT (debugger_message_delay) = JERRY_DEBUGGER_MESSAGE_FREQUENCY;

  uint8_t *recv_buffer_p = JERRY_CONTEXT (debugger_receive_buffer);
  bool resume_exec = false;
  uint8_t expected_message_type = 0;
  void *message_data = NULL;

  while (true)
  {
    uint32_t offset = JERRY_CONTEXT (debugger_receive_buffer_offset);

    ssize_t byte_recv = recv (JERRY_CONTEXT (debugger_connection),
                              recv_buffer_p + offset,
                              JERRY_DEBUGGER_MAX_BUFFER_SIZE - offset,
                              0);

    if (byte_recv < 0)
    {
      if (errno != EWOULDBLOCK)
      {
        jerry_debugger_close_connection_tcp (true);
        return true;
      }

      byte_recv = 0;
    }

    offset += (uint32_t) byte_recv;
    JERRY_CONTEXT (debugger_receive_buffer_offset) = (uint16_t) offset;

    if (offset < sizeof (jerry_debugger_receive_header_t))
    {
      if (expected_message_type != 0)
      {
        continue;
      }

      return resume_exec;
    }

    if ((recv_buffer_p[0] & ~JERRY_DEBUGGER_WEBSOCKET_OPCODE_MASK) != JERRY_DEBUGGER_WEBSOCKET_FIN_BIT
        || (recv_buffer_p[1] & JERRY_DEBUGGER_WEBSOCKET_LENGTH_MASK) > JERRY_DEBUGGER_MAX_RECEIVE_SIZE
        || !(recv_buffer_p[1] & JERRY_DEBUGGER_WEBSOCKET_MASK_BIT))
    {
      jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Unsupported Websocket message.\n");
      jerry_debugger_close_connection ();
      return true;
    }

    if ((recv_buffer_p[0] & JERRY_DEBUGGER_WEBSOCKET_OPCODE_MASK) != JERRY_DEBUGGER_WEBSOCKET_BINARY_FRAME)
    {
      jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Unsupported Websocket opcode.\n");
      jerry_debugger_close_connection ();
      return true;
    }

    uint32_t message_size = (uint32_t) (recv_buffer_p[1] & JERRY_DEBUGGER_WEBSOCKET_LENGTH_MASK);
    uint32_t message_total_size = (uint32_t) (message_size + sizeof (jerry_debugger_receive_header_t));

    if (offset < message_total_size)
    {
      if (expected_message_type != 0)
      {
        continue;
      }

      return resume_exec;
    }

    /* Unmask data bytes. */
    uint8_t *data_p = recv_buffer_p + sizeof (jerry_debugger_receive_header_t);
    const uint8_t *mask_p = data_p - JERRY_DEBUGGER_WEBSOCKET_MASK_SIZE;
    const uint8_t *mask_end_p = data_p;
    const uint8_t *data_end_p = data_p + message_size;

    while (data_p < data_end_p)
    {
      /* Invert certain bits with xor operation. */
      *data_p = *data_p ^ *mask_p;

      data_p++;
      mask_p++;

      if (mask_p >= mask_end_p)
      {
        mask_p -= JERRY_DEBUGGER_WEBSOCKET_MASK_SIZE;
      }
    }

    /* The jerry_debugger_process_message function is inlined
     * so passing these arguments is essentially free. */
    if (!jerry_debugger_process_message (recv_buffer_p + sizeof (jerry_debugger_receive_header_t),
                                         message_size,
                                         &resume_exec,
                                         &expected_message_type,
                                         &message_data))
    {
      return true;
    }

    if (message_total_size < offset)
    {
      memmove (recv_buffer_p,
               recv_buffer_p + message_total_size,
               offset - message_total_size);
    }

    JERRY_CONTEXT (debugger_receive_buffer_offset) = (uint16_t) (offset - message_total_size);
  }
} /* jerry_debugger_receive */

#endif /* JERRY_DEBUGGER */
