/*
 * Copyright (C) 2022 SUSE Software Solutions Germany GmbH
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Written by:
 *     Joan Torres <joan.torres@suse.com>
 */

#include "config.h"

#include "grd-rdp-routing-token.h"

#include <freerdp/freerdp.h>
#include <sys/socket.h>

#define MAX_PEEK_TIME_MS 2000
#define PROTOCOL_RDSTLS 0x00000004

typedef struct _RoutingTokenContext
{
  GrdRdpServer *rdp_server;
  GSocketConnection *connection;

  GCancellable *cancellable;
  unsigned int abort_peek_source_id;

  GCancellable *server_cancellable;

  gboolean requested_rdstls;
} RoutingTokenContext;

static void
wstream_free_full (wStream *s);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (wStream, wstream_free_full)

static void
wstream_free_full (wStream *s)
{
  Stream_Free (s, TRUE);
}

static int
find_cr_lf (const char *buffer,
            int         length)
{
  int i;

  for (i = 0; i < length - 1; ++i)
    {
      if (buffer[i] == 0x0D && buffer[i + 1] == 0x0A)
        return i;
    }

  return -1;
}

static gboolean
peek_bytes (int            fd,
            uint8_t       *buffer,
            int            length,
            GCancellable  *cancellable,
            GError       **error)
{
  GPollFD poll_fds[2] = {};
  int n_fds = 0;
  int ret;

  poll_fds[n_fds].fd = fd;
  poll_fds[n_fds].events = G_IO_IN;
  n_fds++;

  if (!g_cancellable_make_pollfd (cancellable, &poll_fds[n_fds]))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Failure preparing the cancellable for pollfd");
      return FALSE;
    }

  n_fds++;

  do
    {
      do
        ret = g_poll (poll_fds, n_fds, MAX_PEEK_TIME_MS);
      while (ret == -1 && errno == EINTR);

      if (ret == -1)
        {
          g_set_error (error, G_IO_ERROR,
                       g_io_error_from_errno (errno),
                       "On poll command: %s", strerror (errno));
          g_cancellable_release_fd (cancellable);
          return FALSE;
        }

      if (g_cancellable_is_cancelled (cancellable))
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                       "Cancelled");
          g_cancellable_release_fd (cancellable);
          return FALSE;
        }

      do
        ret = recv (fd, (void *) buffer, (size_t) length, MSG_PEEK);
      while (ret == -1 && errno == EINTR);

      if (ret == -1)
        {
          if (errno == EAGAIN || errno == EWOULDBLOCK)
            continue;

          g_set_error (error, G_IO_ERROR,
                       g_io_error_from_errno (errno),
                       "On recv command: %s", strerror (errno));
          g_cancellable_release_fd (cancellable);
          return FALSE;
        }

    }
  while (ret < length);

  g_cancellable_release_fd (cancellable);

  return TRUE;
}

static char *
get_routing_token_without_prefix (char   *buffer,
                                  size_t  buffer_length,
                                  size_t *routing_token_length)
{
  g_autofree char *peeked_prefix = NULL;
  g_autofree char *prefix = NULL;
  size_t prefix_length;

  prefix = g_strdup ("Cookie: msts=");
  prefix_length = strlen (prefix);

  if (buffer_length < prefix_length)
    return NULL;

  peeked_prefix = g_strndup (buffer, prefix_length);
  if (g_strcmp0 (peeked_prefix, prefix) != 0)
    return NULL;

  *routing_token_length = find_cr_lf (buffer, buffer_length);
  if (*routing_token_length == -1)
    return NULL;

  return g_strndup (buffer + prefix_length,
                    *routing_token_length - prefix_length);
}

static gboolean
peek_routing_token (int            fd,
                    char         **routing_token,
                    gboolean      *requested_rdstls,
                    GCancellable  *cancellable,
                    GError       **error)
{
  g_autoptr (wStream) s = NULL;

  /* TPKT values */
  uint8_t  version;
  uint16_t tpkt_length;

  /* x224Crq values */
  uint8_t  length_indicator;
  uint8_t  cr_cdt;
  uint16_t dst_ref;
  uint8_t  class_opt;

  size_t routing_token_length;

  /* rdpNegReq values */
  uint8_t rdp_neg_type;
  uint16_t rdp_neg_length;
  uint32_t requested_protocols;

  /* Peek TPKT Header */
  s = Stream_New (NULL, 4);
  g_assert (s);

  if (!peek_bytes (fd, Stream_Buffer (s), 4, cancellable, error))
    return FALSE;

  Stream_Read_UINT8 (s, version);
  Stream_Seek (s, 1);
  Stream_Read_UINT16_BE (s, tpkt_length);

  if (version != 3)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "The TPKT Header doesn't have version 3");
      return FALSE;
    }
  if (tpkt_length < 4 + 7)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "The x224Crq TPDU length is too short");
      return FALSE;
    }

  /* Peek full PDU */
  Stream_Free (s, TRUE);
  s = Stream_New (NULL, tpkt_length);
  g_assert (s);

  if (!peek_bytes (fd, Stream_Buffer (s), tpkt_length, cancellable, error))
    return FALSE;

  Stream_Seek (s, 4);

  /* Check x224Crq */
  Stream_Read_UINT8 (s, length_indicator);
  Stream_Read_UINT8 (s, cr_cdt);
  Stream_Read_UINT16 (s, dst_ref);
  Stream_Seek (s, 2);
  Stream_Read_UINT8 (s, class_opt);
  if (tpkt_length - 5 != length_indicator ||
      cr_cdt != 0xE0 ||
      dst_ref != 0 ||
      (class_opt & 0xFC) != 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Wrong info on x224Crq");
      return FALSE;
    }

  /* Check routingToken */
  *routing_token =
    get_routing_token_without_prefix ((char *) Stream_Pointer (s),
                                      Stream_GetRemainingLength (s),
                                      &routing_token_length);
  if (!(*routing_token))
    return TRUE;

  /* Check rdpNegReq */
  Stream_Seek (s, routing_token_length + 2);
  if (Stream_GetRemainingLength (s) < 8)
    return TRUE;

  Stream_Read_UINT8 (s, rdp_neg_type);
  Stream_Seek (s, 1);
  Stream_Read_UINT16 (s, rdp_neg_length);
  Stream_Read_UINT32 (s, requested_protocols);
  if (rdp_neg_type != 0x01 || rdp_neg_length != 8)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Wrong info on rdpNegReq");
      return FALSE;
    }

  *requested_rdstls = !!(requested_protocols & PROTOCOL_RDSTLS);

  return TRUE;
}

static void
peek_routing_token_in_thread (GTask        *task,
                              gpointer      source_object,
                              gpointer      task_data,
                              GCancellable *cancellable)
{
  RoutingTokenContext *routing_token_context = task_data;
  GSocket *socket;
  int fd;
  char *routing_token = NULL;
  GError *error = NULL;

  socket = g_socket_connection_get_socket (routing_token_context->connection);
  fd = g_socket_get_fd (socket);

  if (!peek_routing_token (fd,
                           &routing_token,
                           &routing_token_context->requested_rdstls,
                           routing_token_context->cancellable,
                           &error))
    g_task_return_error (task, error);
  else
    g_task_return_pointer (task, routing_token, g_free);
}

static gboolean
abort_peek_routing_token (gpointer user_data)
{
  RoutingTokenContext *routing_token_context = user_data;

  g_assert (routing_token_context->abort_peek_source_id);

  g_debug ("RoutingToken: Aborting current peek operation "
           "(Timeout reached)");

  g_cancellable_cancel (routing_token_context->cancellable);

  routing_token_context->abort_peek_source_id = 0;

  return G_SOURCE_REMOVE;
}

static void
clear_routing_token_context (gpointer data)
{
  RoutingTokenContext *routing_token_context = data;

  g_clear_object (&routing_token_context->connection);
  g_clear_object (&routing_token_context->server_cancellable);
  g_clear_object (&routing_token_context->cancellable);
  g_clear_handle_id (&routing_token_context->abort_peek_source_id,
                     g_source_remove);

  g_free (routing_token_context);
}

void
grd_routing_token_peek_async (GrdRdpServer        *rdp_server,
                              GSocketConnection   *connection,
                              GCancellable        *cancellable,
                              GAsyncReadyCallback  on_finished_callback)
{
  RoutingTokenContext *routing_token_context;
  GTask *task;

  routing_token_context = g_new0 (RoutingTokenContext, 1);
  routing_token_context->rdp_server = rdp_server;
  routing_token_context->connection = g_object_ref (connection);
  routing_token_context->cancellable = g_cancellable_new ();
  routing_token_context->server_cancellable = g_object_ref (cancellable);

  task = g_task_new (NULL, NULL, on_finished_callback, NULL);
  g_task_set_task_data (task, routing_token_context, clear_routing_token_context);
  g_task_run_in_thread (task, peek_routing_token_in_thread);
  g_object_unref (task);

  routing_token_context->abort_peek_source_id =
    g_timeout_add (MAX_PEEK_TIME_MS,
                   abort_peek_routing_token,
                   routing_token_context);
}

char *
grd_routing_token_peek_finish (GAsyncResult       *result,
                               GrdRdpServer      **rdp_server,
                               GSocketConnection **connection,
                               GCancellable      **server_cancellable,
                               gboolean           *requested_rdstls,
                               GError            **error)
{
  RoutingTokenContext *routing_token_context =
    g_task_get_task_data (G_TASK (result));

  *rdp_server = routing_token_context->rdp_server;
  *connection = routing_token_context->connection;
  *server_cancellable = routing_token_context->server_cancellable;
  *requested_rdstls = routing_token_context->requested_rdstls;

  return g_task_propagate_pointer (G_TASK (result), error);
}
