/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Cockpit 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 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit 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 Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "cockpitchannelresponse.h"

#include "common/cockpitwebinject.h"
#include "common/cockpitwebserver.h"
#include "common/cockpitwebresponse.h"

#include <string.h>

typedef struct {
  CockpitWebService *service;
  gchar *base_path;
  gchar *host;
} CockpitChannelInject;

static void
cockpit_channel_inject_free (gpointer data)
{
  CockpitChannelInject *inject = data;

  if (inject)
    {
      if (inject->service)
        g_object_remove_weak_pointer (G_OBJECT (inject->service), (gpointer *)&inject->service);
      g_free (inject->base_path);
      g_free (inject->host);
      g_free (inject);
    }
}

static CockpitChannelInject *
cockpit_channel_inject_new (CockpitWebService *service,
                            const gchar *path,
                            const gchar *host)
{
  CockpitChannelInject *inject = g_new (CockpitChannelInject, 1);
  inject->service = service;
  g_object_add_weak_pointer (G_OBJECT (inject->service), (gpointer *)&inject->service);
  inject->base_path = g_strdup (path);
  inject->host = g_strdup (host);
  return inject;
}

static void
cockpit_channel_inject_update_checksum (CockpitChannelInject *inject,
                                        GHashTable *headers)
{
  const gchar *checksum = g_hash_table_lookup (headers, COCKPIT_CHECKSUM_HEADER);

  if (checksum)
    cockpit_web_service_set_host_checksum (inject->service, inject->host, checksum);

  /* No need to send our custom header outside of cockpit */
  g_hash_table_remove (headers, COCKPIT_CHECKSUM_HEADER);
}

static void
cockpit_channel_inject_perform (CockpitChannelInject *inject,
                                CockpitWebResponse *response,
                                CockpitTransport *transport)
{
  static const gchar *marker = "<head>";
  CockpitWebFilter *filter;
  CockpitCreds *creds;
  gchar *prefixed_application = NULL;
  const gchar *checksum;
  GString *str;
  GBytes *base;

  if (!inject->base_path)
    return;

  str = g_string_new ("");
  creds = cockpit_web_service_get_creds (inject->service);
  if (cockpit_web_response_get_url_root (response))
    {
      prefixed_application = g_strdup_printf ("%s/%s",
                                              cockpit_web_response_get_url_root (response),
                                              cockpit_creds_get_application (creds));
    }
  else
    {
      prefixed_application = g_strdup_printf ("/%s", cockpit_creds_get_application (creds));
    }

  checksum = cockpit_web_service_get_checksum (inject->service, inject->host);
  if (checksum)
    {
      g_string_printf (str, "\n    <base href=\"%s/$%s%s\">",
                       prefixed_application,
                       checksum, inject->base_path);
    }
  else
    {
      g_string_printf (str, "\n    <base href=\"%s/@%s%s\">",
                       prefixed_application, inject->host, inject->base_path);
    }

  base = g_string_free_to_bytes (str);
  filter = cockpit_web_inject_new (marker, base, 1);
  g_bytes_unref (base);

  cockpit_web_response_add_filter (response, filter);
  g_object_unref (filter);
  g_free (prefixed_application);
}

typedef struct {
  const gchar *logname;
  gchar *channel;
  JsonObject *open;

  CockpitWebResponse *response;
  GHashTable *headers;

  CockpitTransport *transport;
  gulong transport_recv;
  gulong transport_control;
  gulong transport_closed;

  /* Set when injecting data into response */
  CockpitChannelInject *inject;
} CockpitChannelResponse;

static gboolean
ensure_headers (CockpitChannelResponse *chesp,
                guint status,
                const gchar *reason)
{
  if (cockpit_web_response_get_state (chesp->response) == COCKPIT_WEB_RESPONSE_READY)
    {
      if (chesp->inject && chesp->inject->service)
        {
          cockpit_channel_inject_update_checksum (chesp->inject, chesp->headers);
          cockpit_channel_inject_perform (chesp->inject, chesp->response, chesp->transport);
        }
      cockpit_web_response_headers_full (chesp->response, status, reason, -1, chesp->headers);
      return TRUE;
    }

  return FALSE;
}

static void
cockpit_channel_response_close (CockpitChannelResponse *chesp,
                                const gchar *problem)
{
  CockpitWebResponding state;

  /* Ensure no more signals arrive about our response */
  g_signal_handler_disconnect (chesp->transport, chesp->transport_recv);
  g_signal_handler_disconnect (chesp->transport, chesp->transport_control);
  g_signal_handler_disconnect (chesp->transport, chesp->transport_closed);

  /* The web response should not yet be complete */
  state = cockpit_web_response_get_state (chesp->response);

  if (problem == NULL)
    {
      /* Closed without any data */
      if (state == COCKPIT_WEB_RESPONSE_READY)
        {
          ensure_headers (chesp, 204, "OK");
          cockpit_web_response_complete (chesp->response);
          g_debug ("%s: no content in external channel", chesp->logname);
        }
      else if (state < COCKPIT_WEB_RESPONSE_COMPLETE)
        {
          g_message ("%s: truncated data in external channel", chesp->logname);
          cockpit_web_response_abort (chesp->response);
        }
      else
        {
          g_debug ("%s: completed serving external channel", chesp->logname);
        }
    }
  else if (state == COCKPIT_WEB_RESPONSE_READY)
    {
      if (g_str_equal (problem, "not-found"))
        {
          g_debug ("%s: not found", chesp->logname);
          cockpit_web_response_error (chesp->response, 404, NULL, NULL);
        }
      else if (g_str_equal (problem, "no-host") ||
               g_str_equal (problem, "no-cockpit") ||
               g_str_equal (problem, "unknown-hostkey") ||
               g_str_equal (problem, "authentication-failed") ||
               g_str_equal (problem, "disconnected"))
        {
          g_debug ("%s: remote server unavailable: %s", chesp->logname, problem);
          cockpit_web_response_error (chesp->response, 502, NULL, NULL);
        }
      else
        {
          g_message ("%s: external channel failed: %s", chesp->logname, problem);
          cockpit_web_response_error (chesp->response, 500, NULL, NULL);
        }
    }
  else
    {
      if (g_str_equal (problem, "disconnected") || g_str_equal (problem, "terminated"))
        g_debug ("%s: failure while serving external channel: %s", chesp->logname, problem);
      else
        g_message ("%s: failure while serving external channel: %s", chesp->logname, problem);
      cockpit_web_response_abort (chesp->response);
    }

  g_object_unref (chesp->response);
  g_object_unref (chesp->transport);
  g_hash_table_unref (chesp->headers);
  cockpit_channel_inject_free (chesp->inject);
  json_object_unref (chesp->open);
  g_free (chesp->channel);
  g_free (chesp);
}

static gboolean
on_transport_recv (CockpitTransport *transport,
                   const gchar *channel,
                   GBytes *payload,
                   CockpitChannelResponse *chesp)
{
  if (channel && g_str_equal (channel, chesp->channel))
    {
      ensure_headers (chesp, 200, "OK");
      cockpit_web_response_queue (chesp->response, payload);
      return TRUE;
    }

  return FALSE;
}

static void
object_to_headers (JsonObject *object,
                   const gchar *header,
                   JsonNode *node,
                   gpointer user_data)
{
  GHashTable *headers = user_data;
  const gchar *value = json_node_get_string (node);

  g_return_if_fail (value != NULL);

  if (g_ascii_strcasecmp (header, "Content-Length") == 0 ||
      g_ascii_strcasecmp (header, "Connection") == 0)
    return;

  g_hash_table_insert (headers, g_strdup (header), g_strdup (value));
}

static gboolean
parse_httpstream_response (CockpitChannelResponse *chesp,
                           JsonObject *object,
                           gint64 *status,
                           const gchar **reason)
{
  JsonNode *node;

  if (!cockpit_json_get_int (object, "status", 200, status) ||
      !cockpit_json_get_string (object, "reason", NULL, reason))
    {
      g_warning ("%s: received invalid httpstream response", chesp->logname);
      return FALSE;
    }

  node = json_object_get_member (object, "headers");
  if (node)
    {
      if (!JSON_NODE_HOLDS_OBJECT (node))
        {
          g_warning ("%s: received invalid httpstream headers", chesp->logname);
          return FALSE;
        }
      json_object_foreach_member (json_node_get_object (node), object_to_headers, chesp->headers);
    }

  return TRUE;
}

static gboolean
on_httpstream_recv (CockpitTransport *transport,
                    const gchar *channel,
                    GBytes *payload,
                    CockpitChannelResponse *chesp)
{
  GError *error = NULL;
  JsonObject *object;
  gint64 status;
  const gchar *reason;

  if (!channel || !g_str_equal (channel, chesp->channel))
    return FALSE;

  g_return_val_if_fail (cockpit_web_response_get_state (chesp->response) == COCKPIT_WEB_RESPONSE_READY, FALSE);

  /* First response payload message is meta data, then switch to actual data */
  g_signal_handler_disconnect (chesp->transport, chesp->transport_recv);
  chesp->transport_recv = g_signal_connect (chesp->transport, "recv", G_CALLBACK (on_transport_recv), chesp);

  object = cockpit_json_parse_bytes (payload, &error);
  if (error)
    {
      g_warning ("%s: couldn't parse http-stream1 header payload: %s", chesp->logname, error->message);
      cockpit_web_response_error (chesp->response, 500, NULL, NULL);
      g_error_free (error);
      return TRUE;
    }

  if (parse_httpstream_response (chesp, object, &status, &reason))
    {
      if (!ensure_headers (chesp, status, reason))
        g_return_val_if_reached (FALSE);
    }
  else
    {
      cockpit_web_response_error (chesp->response, 500, NULL, NULL);
    }

  json_object_unref (object);
  return TRUE;
}

static gboolean
on_transport_control (CockpitTransport *transport,
                      const gchar *command,
                      const gchar *channel,
                      JsonObject *options,
                      GBytes *message,
                      CockpitChannelResponse *chesp)
{
  const gchar *problem = NULL;

  if (!channel || !g_str_equal (channel, chesp->channel))
    return FALSE; /* not handled */

  if (g_str_equal (command, "done"))
    {
      ensure_headers (chesp, 200, "OK");
      cockpit_web_response_complete (chesp->response);
      return TRUE;
    }
  else if (g_str_equal (command, "close"))
    {
      if (!cockpit_json_get_string (options, "problem", NULL, &problem))
        {
          g_message ("%s: received close command with invalid problem", chesp->logname);
          problem = "disconnected";
        }
      cockpit_channel_response_close (chesp, problem);
    }
  else
    {
      /* Ignore other control messages */
    }

  return TRUE; /* handled */
}

static gboolean
on_httpstream_control (CockpitTransport *transport,
                       const gchar *command,
                       const gchar *channel,
                       JsonObject *options,
                       GBytes *message,
                       CockpitChannelResponse *chesp)
{
  gint64 status;
  const gchar *reason;

  if (!channel || !g_str_equal (channel, chesp->channel))
    return FALSE; /* not handled */

  if (g_str_equal (command, "response"))
    {
      if (parse_httpstream_response (chesp, options, &status, &reason))
        {
          if (!ensure_headers (chesp, status, reason))
            g_return_val_if_reached (FALSE);
        }
      else
        {
          cockpit_web_response_error (chesp->response, 500, NULL, NULL);
        }
      return TRUE;
    }

  return on_transport_control (transport, command, channel, options, message, chesp);
}


static void
on_transport_closed (CockpitTransport *transport,
                     const gchar *problem,
                     CockpitChannelResponse *chesp)
{
  cockpit_channel_response_close (chesp, problem ? problem : "disconnected");
}

static CockpitChannelResponse *
cockpit_channel_response_create (CockpitWebService *service,
                                 CockpitWebResponse *response,
                                 CockpitTransport *transport,
                                 const gchar *logname,
                                 GHashTable *headers,
                                 JsonObject *open)
{
  CockpitChannelResponse *chesp;
  const gchar *payload;
  JsonObject *done;
  GBytes *bytes;

  payload = json_object_get_string_member (open, "payload");

  chesp = g_new0 (CockpitChannelResponse, 1);
  chesp->response = g_object_ref (response);
  chesp->transport = g_object_ref (transport);
  chesp->headers = g_hash_table_ref (headers);
  chesp->channel = cockpit_web_service_unique_channel (service);
  chesp->open = json_object_ref (open);

  if (!cockpit_json_get_string (open, "path", chesp->channel, &chesp->logname))
    chesp->logname = chesp->channel;

  json_object_set_string_member (open, "command", "open");
  json_object_set_string_member (open, "channel", chesp->channel);

  /* Special handling for http-stream1, splice in headers, handle injection */
  if (g_strcmp0 (payload, "http-stream1") == 0)
    chesp->transport_recv = g_signal_connect (transport, "recv", G_CALLBACK (on_httpstream_recv), chesp);
  else
    chesp->transport_recv = g_signal_connect (transport, "recv", G_CALLBACK (on_transport_recv), chesp);

  /* Special handling for http-stream2, splice in headers, handle injection */
  if (g_strcmp0 (payload, "http-stream2") == 0)
    chesp->transport_control = g_signal_connect (transport, "control", G_CALLBACK (on_httpstream_control), chesp);
  else
    chesp->transport_control = g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), chesp);

  chesp->transport_closed = g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), chesp);

  bytes = cockpit_json_write_bytes (chesp->open);
  cockpit_transport_send (transport, NULL, bytes);
  g_bytes_unref (bytes);

  done = cockpit_transport_build_json ("command", "done", "channel", chesp->channel, NULL);
  bytes = cockpit_json_write_bytes (done);
  json_object_unref (done);
  cockpit_transport_send (transport, NULL, bytes);
  g_bytes_unref (bytes);

  return chesp;
}

static gboolean
is_resource_a_package_file (const gchar *path)
{
  return path && path[0] && strchr (path + 1, '/') != NULL;
}

static gboolean
parse_host_and_etag (CockpitWebService *service,
                     GHashTable *headers,
                     const gchar *where,
                     const gchar *path,
                     const gchar **host,
                     gchar **etag)
{
  gchar **languages = NULL;
  gboolean translatable;
  gchar *language;

  /* Parse the language out of the CockpitLang cookie and set Accept-Language */
  language = cockpit_web_server_parse_cookie (headers, "CockpitLang");
  if (language)
    g_hash_table_replace (headers, g_strdup ("Accept-Language"), language);

  if (!where)
    {
      *host = "localhost";
      *etag = NULL;
      return TRUE;
    }
  if (where[0] == '@')
    {
      *host = where + 1;
      *etag = NULL;
      return TRUE;
    }

  if (!where || where[0] != '$')
    return FALSE;

  *host = cockpit_web_service_get_host (service, where + 1);
  if (!*host)
    return FALSE;

  /* Top level resources (like the /manifests) are not translatable */
  translatable = is_resource_a_package_file (path);

  /* The ETag contains the language setting */
  if (translatable)
    {
      languages = cockpit_web_server_parse_languages (headers, "C");
      *etag = g_strdup_printf ("\"%s-%s\"", where, languages[0]);
      g_strfreev (languages);
    }
  else
    {
      *etag = g_strdup_printf ("\"%s\"", where);
    }

  return TRUE;
}

void
cockpit_channel_response_serve (CockpitWebService *service,
                                GHashTable *in_headers,
                                CockpitWebResponse *response,
                                const gchar *where,
                                const gchar *path)
{
  CockpitChannelResponse *chesp = NULL;
  CockpitTransport *transport = NULL;
  CockpitCacheType cache_type = COCKPIT_WEB_RESPONSE_CACHE_PRIVATE;
  const gchar *host = NULL;
  const gchar *pragma;
  gchar *quoted_etag = NULL;
  GHashTable *out_headers = NULL;
  gchar *val = NULL;
  gboolean handled = FALSE;
  GHashTableIter iter;
  JsonObject *object = NULL;
  JsonObject *heads;
  GIOStream *connection;
  const gchar *protocol;
  const gchar *http_host = "localhost";
  gchar *channel = NULL;
  gpointer key;
  gpointer value;

  g_return_if_fail (COCKPIT_IS_WEB_SERVICE (service));
  g_return_if_fail (in_headers != NULL);
  g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response));
  g_return_if_fail (path != NULL);

  /* Where might be NULL, but that's still valid */
  if (!parse_host_and_etag (service, in_headers, where, path, &host, &quoted_etag))
    {
      /* Did not recognize the where */
      goto out;
    }

  if (quoted_etag)
    {
      cache_type = COCKPIT_WEB_RESPONSE_CACHE_FOREVER;
      pragma = g_hash_table_lookup (in_headers, "Pragma");

      if ((!pragma || !strstr (pragma, "no-cache")) &&
           g_strcmp0 (g_hash_table_lookup (in_headers, "If-None-Match"), quoted_etag) == 0)
        {
          cockpit_web_response_headers (response, 304, "Not Modified", 0, "ETag", quoted_etag, NULL);
          cockpit_web_response_complete (response);
          handled = TRUE;
          goto out;
        }
    }

  cockpit_web_response_set_cache_type (response, cache_type);
  object = cockpit_transport_build_json ("command", "open",
                                         "payload", "http-stream1",
                                         "internal", "packages",
                                         "method", "GET",
                                         "host", host,
                                         "path", path,
                                         "binary", "raw",
                                         NULL);

  transport = cockpit_web_service_get_transport (service);
  if (!transport)
    goto out;

  out_headers = cockpit_web_server_new_table ();

  channel = cockpit_web_service_unique_channel (service);
  json_object_set_string_member (object, "channel", channel);

  if (quoted_etag)
    {
      /*
       * If we have a checksum, then use it as an ETag. It is intentional that
       * a cockpit-bridge version could (in the future) override this.
       */
      g_hash_table_insert (out_headers, g_strdup ("ETag"), quoted_etag);
      quoted_etag = NULL;
    }

  heads = json_object_new ();

  g_hash_table_iter_init (&iter, in_headers);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      val = NULL;

      if (g_ascii_strcasecmp (key, "Cookie") == 0 ||
          g_ascii_strcasecmp (key, "Referer") == 0 ||
          g_ascii_strcasecmp (key, "Connection") == 0 ||
          g_ascii_strcasecmp (key, "Pragma") == 0 ||
          g_ascii_strcasecmp (key, "Cache-Control") == 0 ||
          g_ascii_strcasecmp (key, "User-Agent") == 0 ||
          g_ascii_strcasecmp (key, "Accept-Charset") == 0 ||
          g_ascii_strcasecmp (key, "Accept-Ranges") == 0 ||
          g_ascii_strcasecmp (key, "Content-Length") == 0 ||
          g_ascii_strcasecmp (key, "Content-MD5") == 0 ||
          g_ascii_strcasecmp (key, "Content-Range") == 0 ||
          g_ascii_strcasecmp (key, "Range") == 0 ||
          g_ascii_strcasecmp (key, "TE") == 0 ||
          g_ascii_strcasecmp (key, "Trailer") == 0 ||
          g_ascii_strcasecmp (key, "Upgrade") == 0 ||
          g_ascii_strcasecmp (key, "Transfer-Encoding") == 0 ||
          g_ascii_strcasecmp (key, "X-Forwarded-For") == 0 ||
          g_ascii_strcasecmp (key, "X-Forwarded-Host") == 0 ||
          g_ascii_strcasecmp (key, "X-Forwarded-Protocol") == 0)
        continue;

      if (g_ascii_strcasecmp (key, "Host") == 0)
        http_host = (gchar *) value;
      else
        json_object_set_string_member (heads, key, value);

      g_free (val);
    }

  /* Send along the HTTP scheme the package should assume is accessing things */
  connection = cockpit_web_response_get_stream (response);
  protocol = cockpit_web_response_get_protocol (connection, in_headers);

  json_object_set_string_member (heads, "Host", host);
  json_object_set_string_member (heads, "X-Forwarded-Proto", protocol);
  json_object_set_string_member (heads, "X-Forwarded-Host", http_host);

  json_object_set_object_member (object, "headers", heads);

  chesp = cockpit_channel_response_create (service, response, transport,
                                           cockpit_web_response_get_path (response),
                                           out_headers, object);

  chesp->inject = cockpit_channel_inject_new (service,
                                              where ? NULL : path,
                                              host);
  handled = TRUE;

out:
  if (object)
    json_object_unref (object);
  g_free (quoted_etag);
  if (out_headers)
    g_hash_table_unref (out_headers);
  g_free (channel);

  if (!handled)
    cockpit_web_response_error (response, 404, NULL, NULL);
}

void
cockpit_channel_response_open (CockpitWebService *service,
                               GHashTable *in_headers,
                               CockpitWebResponse *response,
                               JsonObject *open)
{
  CockpitTransport *transport;
  WebSocketDataType data_type;
  GHashTable *headers;
  const gchar *content_type;
  const gchar *content_encoding;
  const gchar *content_disposition;

  /* Parse the external */
  if (!cockpit_web_service_parse_external (open, &content_type, &content_encoding, &content_disposition, NULL))
    {
      cockpit_web_response_error (response, 400, NULL, "Bad channel request");
      return;
    }

  transport = cockpit_web_service_get_transport (service);
  if (!transport)
    {
      cockpit_web_response_error (response, 502, NULL, "Failed to open channel transport");
      return;
    }

  headers = cockpit_web_server_new_table ();

  if (content_disposition)
    g_hash_table_insert (headers, g_strdup ("Content-Disposition"), g_strdup (content_disposition));

  if (!json_object_has_member (open, "binary"))
    json_object_set_string_member (open, "binary", "raw");

  if (!content_type)
    {
      if (!cockpit_web_service_parse_binary (open, &data_type))
        g_return_if_reached ();
      if (data_type == WEB_SOCKET_DATA_TEXT)
        content_type = "text/plain";
      else
        content_type = "application/octet-stream";
    }
  g_hash_table_insert (headers, g_strdup ("Content-Type"), g_strdup (content_type));

  if (content_encoding)
    g_hash_table_insert (headers, g_strdup ("Content-Encoding"), g_strdup (content_encoding));

  /* We shouldn't need to send this part further */
  json_object_remove_member (open, "external");

  cockpit_channel_response_create (service, response, transport, NULL, headers, open);
  g_hash_table_unref (headers);
}
