<commit_before>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2017 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 "common/cockpitauthorize.h"
#include "common/cockpittest.h"
#include "common/cockpiterror.h"
#include "common/cockpitpipe.h"
#include "common/cockpitpipetransport.h"
#include "common/cockpitjson.h"

#include <sys/wait.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>

#define TIMEOUT 120

#define WAIT_UNTIL(cond) \
  G_STMT_START \
    while (!(cond)) g_main_context_iteration (NULL, TRUE); \
  G_STMT_END

#define PASSWORD "<PASSWORD>"

#define INVALID_KEY ""

typedef struct {
  CockpitTransport *transport;
  gboolean closed;

  /* setup_mock_sshd */
  GPid mock_sshd;
  guint16 ssh_port;
  gchar *home_dir;
  gchar *home_ssh_dir;
  gchar *home_knownhosts_file;
  gchar *home_ssh_config_file;
} TestCase;

typedef struct {
    const char *ssh_command;
    const char *mock_sshd_arg;
    const char *client_password;
    const char *hostname;
    const char *username;
    const char *knownhosts_file;
    const char *knownhosts_home;
    const char *knownhosts_sssd;
    const char *knownhosts_sssd_host;
    const char *host_key_authorize; /* authorize x-host-key response for test_problem() */
    const char *config;
    const char *problem;
    gboolean allow_unknown;
    gboolean test_home_ssh_config;
} TestFixture;

static GString *
read_all_into_string (int fd)
{
  GString *input = g_string_new ("");
  gsize len;
  gssize ret;

  for (;;)
    {
      len = input->len;
      g_string_set_size (input, len + 256);
      ret = read (fd, input->str + len, 256);
      if (ret < 0)
        {
          if (errno != EAGAIN)
            {
              g_critical ("couldn't read from mock input: %s", g_strerror (errno));
              g_string_free (input, TRUE);
              return NULL;
            }
        }
      else if (ret == 0)
        {
          return input;
        }
      else
        {
          input->len = len + ret;
          input->str[input->len] = '\0';
        }
    }
}

static void
spawn_setup (gpointer data)
{
  int fd = GPOINTER_TO_INT (data);

  /* Send this signal to all direct child processes, when bridge dies */
  prctl (PR_SET_PDEATHSIG, SIGHUP);

  g_assert_cmpint (dup2 (fd, 0), >, -1);
  g_assert_cmpint (dup2 (fd, 1), >, -1);

  close (fd);
}

static void
setup_mock_sshd (TestCase *tc,
                 gconstpointer data)
{
  const TestFixture *fixture = data;
  GError *error = NULL;
  GString *port;
  gchar *endptr;
  guint64 value;
  gint out_fd;

  const gchar *argv[] = {
      BUILDDIR "/mock-sshd",
      "--bind", fixture->hostname ?: "127.0.0.1",
      "--user", g_get_user_name (),
      "--password", PASSWORD,
      fixture->mock_sshd_arg,
      NULL
  };

  g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
                            &tc->mock_sshd, NULL, &out_fd, NULL, &error);
  g_assert_no_error (error);

  /*
   * mock-sshd prints its port on stdout, and then closes stdout
   * This also lets us know when it has initialized.
   */

  port = read_all_into_string (out_fd);
  g_assert (port != NULL);
  close (out_fd);
  g_assert_no_error (error);

  g_strstrip (port->str);
  value = g_ascii_strtoull (port->str, &endptr, 10);
  if (!endptr || *endptr != '\0' || value == 0 || value > G_MAXUSHORT)
      g_critical ("invalid port printed by mock-sshd: %s", port->str);

  tc->ssh_port = (gushort)value;
  g_string_free (port, TRUE);
}

static const TestFixture fixture_mock_echo = {
  .ssh_command = BUILDDIR "/mock-echo"
};

static const TestFixture fixture_cat = {
  .ssh_command =  SRCDIR "/src/ws/mock-cat-with-init"
};

static gchar **
setup_env (const TestFixture *fix)
{
  const gchar *command;
  const gchar *knownhosts_file;
  const gchar *config;
  gchar **env = g_get_environ ();

  config = fix ? fix->config : NULL;
  if (!config)
    config = SRCDIR "/src/ssh/mock-config";
  env = g_environ_setenv (env, "XDG_CONFIG_DIRS", config, TRUE);

  command = fix ? fix->ssh_command : NULL;
  if (!command)
    command = fixture_cat.ssh_command;
  env = g_environ_setenv (env, "COCKPIT_SSH_BRIDGE_COMMAND", command, TRUE);

  if (fix && fix->allow_unknown)
    {
      env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS",
                              "true", TRUE);
    }

  knownhosts_file = fix ? fix->knownhosts_file : NULL;
  if (!knownhosts_file)
      knownhosts_file = SRCDIR "/src/ssh/mock_known_hosts";

  env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE",
                          knownhosts_file, TRUE);
  return env;
}

static CockpitTransport *
start_bridge (gchar **env,
              gchar **argv)
{
  GError *error = NULL;
  int fds[2];

  g_assert_cmpint (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds), ==, 0);
  g_spawn_async_with_pipes (BUILDDIR, argv, env,
                            G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH,
                            spawn_setup, GINT_TO_POINTER (fds[0]),
                            NULL, NULL, NULL, NULL, &error);
  g_assert_no_error (error);
  close (fds[0]);

  return cockpit_pipe_transport_new_fds ("test-ssh", fds[1], fds[1]);
}

static void
on_closed_set_flag (CockpitTransport *transport,
                    const gchar *problem,
                    gpointer user_data)
{
  gboolean *flag = user_data;
  g_assert_cmpstr (problem, ==, NULL);
  g_assert (*flag == FALSE);
  *flag = TRUE;
}

static void
setup (TestCase *tc,
       gconstpointer data)
{
  const TestFixture *fixture = data;
  const gchar *argv[] = { BUILDDIR "/cockpit-ssh", NULL, NULL };
  gchar **env = NULL;
  gchar *host = NULL;
  gchar *path = NULL;

  alarm (TIMEOUT);

  g_assert (fixture != NULL);

  env = setup_env (fixture);
  setup_mock_sshd (tc, data);

  if (tc->ssh_port)
    host = g_strdup_printf ("%s:%d", fixture->hostname ?: "127.0.0.1", tc->ssh_port);
  else
    host = g_strdup (fixture->hostname ?: "127.0.0.1");
  argv[1] = host;

  /* run our tests with temp home dir, to avoid influence from the real ~/.ssh */
  tc->home_dir = g_dir_make_tmp ("home.XXXXXX", NULL);
  g_assert (tc->home_dir != NULL);
  env = g_environ_setenv (env, "HOME", tc->home_dir, TRUE);
  /* use preload library to bend getpwuid_r home dir to the temporary one */
  env = g_environ_setenv (env, "LD_PRELOAD", BUILDDIR "/libpreload-temp-home.so", TRUE);

  tc->home_ssh_dir = g_build_filename (tc->home_dir, ".ssh", NULL);
  g_assert (tc->home_ssh_dir != NULL);

  if (fixture && fixture->knownhosts_home)
    {
      gchar *content;

      tc->home_knownhosts_file = g_build_filename (tc->home_ssh_dir, "known_hosts", NULL);
      g_assert (tc->home_knownhosts_file != NULL);
      g_assert_cmpint (mkdir (tc->home_ssh_dir, 0700), ==, 0);

      content = g_strdup_printf ("[%s]:%d %s\n",
                                 fixture->hostname ?: "127.0.0.1",
                                 (int)tc->ssh_port,
                                 fixture->knownhosts_home);

      g_assert (g_file_set_contents (tc->home_knownhosts_file, content, -1, NULL));

      g_free (content);
    }

#if LIBSSH_085
  if (fixture && fixture->test_home_ssh_config)
    {
      gchar *content;
      gchar **host_port;

      tc->home_ssh_config_file = g_build_filename (tc->home_ssh_dir, "config", NULL);
      if (!fixture->knownhosts_home)
          g_assert_cmpint (mkdir (tc->home_ssh_dir, 0700), ==, 0);
      host_port = g_strsplit (host, ":", 0);

      content = g_strdup_printf ("Host some_host\n "
                                 "\tHostname %s\n"
                                 "\tPort %s\n",
                                 host_port[0],
                                 g_strv_length (host_port) > 1 ? host_port[1] : "22");
      g_assert (g_file_set_contents (tc->home_ssh_config_file, content, -1, NULL));
      g_free (host);
      host = g_strdup ("some_host");
      argv[1] = host;

      g_strfreev (host_port);
      g_free (content);
    }
#endif

  if (fixture && fixture->knownhosts_sssd)
    {
      g_assert (fixture->knownhosts_sssd_host != NULL);
      env = g_environ_setenv (env, "COCKPIT_MOCK_SSS_KNOWN_HOSTS_HOST",
                              fixture->knownhosts_sssd_host, TRUE);
      env = g_environ_setenv (env, "COCKPIT_MOCK_SSS_KNOWN_HOSTS_KEY",
                              fixture->knownhosts_sssd, TRUE);

      path = g_strjoin (":",
                        SRCDIR "/src/ssh/mock-bin",
                        g_environ_getenv (env, "PATH") ?: "",
                        NULL);

      env = g_environ_setenv (env, "PATH", path, TRUE);<commit_msg>ssh: Drop always-true check in test-sshbridge<commit_after>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2017 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 "common/cockpitauthorize.h"
#include "common/cockpittest.h"
#include "common/cockpiterror.h"
#include "common/cockpitpipe.h"
#include "common/cockpitpipetransport.h"
#include "common/cockpitjson.h"

#include <sys/wait.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>

#define TIMEOUT 120

#define WAIT_UNTIL(cond) \
  G_STMT_START \
    while (!(cond)) g_main_context_iteration (NULL, TRUE); \
  G_STMT_END

#define PASSWORD "<PASSWORD>"

#define INVALID_KEY ""

typedef struct {
  CockpitTransport *transport;
  gboolean closed;

  /* setup_mock_sshd */
  GPid mock_sshd;
  guint16 ssh_port;
  gchar *home_dir;
  gchar *home_ssh_dir;
  gchar *home_knownhosts_file;
  gchar *home_ssh_config_file;
} TestCase;

typedef struct {
    const char *ssh_command;
    const char *mock_sshd_arg;
    const char *client_password;
    const char *hostname;
    const char *username;
    const char *knownhosts_file;
    const char *knownhosts_home;
    const char *knownhosts_sssd;
    const char *knownhosts_sssd_host;
    const char *host_key_authorize; /* authorize x-host-key response for test_problem() */
    const char *config;
    const char *problem;
    gboolean allow_unknown;
    gboolean test_home_ssh_config;
} TestFixture;

static GString *
read_all_into_string (int fd)
{
  GString *input = g_string_new ("");
  gsize len;
  gssize ret;

  for (;;)
    {
      len = input->len;
      g_string_set_size (input, len + 256);
      ret = read (fd, input->str + len, 256);
      if (ret < 0)
        {
          if (errno != EAGAIN)
            {
              g_critical ("couldn't read from mock input: %s", g_strerror (errno));
              g_string_free (input, TRUE);
              return NULL;
            }
        }
      else if (ret == 0)
        {
          return input;
        }
      else
        {
          input->len = len + ret;
          input->str[input->len] = '\0';
        }
    }
}

static void
spawn_setup (gpointer data)
{
  int fd = GPOINTER_TO_INT (data);

  /* Send this signal to all direct child processes, when bridge dies */
  prctl (PR_SET_PDEATHSIG, SIGHUP);

  g_assert_cmpint (dup2 (fd, 0), >, -1);
  g_assert_cmpint (dup2 (fd, 1), >, -1);

  close (fd);
}

static void
setup_mock_sshd (TestCase *tc,
                 gconstpointer data)
{
  const TestFixture *fixture = data;
  GError *error = NULL;
  GString *port;
  gchar *endptr;
  guint64 value;
  gint out_fd;

  const gchar *argv[] = {
      BUILDDIR "/mock-sshd",
      "--bind", fixture->hostname ?: "127.0.0.1",
      "--user", g_get_user_name (),
      "--password", PASSWORD,
      fixture->mock_sshd_arg,
      NULL
  };

  g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
                            &tc->mock_sshd, NULL, &out_fd, NULL, &error);
  g_assert_no_error (error);

  /*
   * mock-sshd prints its port on stdout, and then closes stdout
   * This also lets us know when it has initialized.
   */

  port = read_all_into_string (out_fd);
  g_assert (port != NULL);
  close (out_fd);
  g_assert_no_error (error);

  g_strstrip (port->str);
  value = g_ascii_strtoull (port->str, &endptr, 10);
  if (!endptr || *endptr != '\0' || value == 0 || value > G_MAXUSHORT)
      g_critical ("invalid port printed by mock-sshd: %s", port->str);

  tc->ssh_port = (gushort)value;
  g_string_free (port, TRUE);
}

static const TestFixture fixture_mock_echo = {
  .ssh_command = BUILDDIR "/mock-echo"
};

static const TestFixture fixture_cat = {
  .ssh_command =  SRCDIR "/src/ws/mock-cat-with-init"
};

static gchar **
setup_env (const TestFixture *fix)
{
  const gchar *command;
  const gchar *knownhosts_file;
  const gchar *config;
  gchar **env = g_get_environ ();

  config = fix ? fix->config : NULL;
  if (!config)
    config = SRCDIR "/src/ssh/mock-config";
  env = g_environ_setenv (env, "XDG_CONFIG_DIRS", config, TRUE);

  command = fix ? fix->ssh_command : NULL;
  if (!command)
    command = fixture_cat.ssh_command;
  env = g_environ_setenv (env, "COCKPIT_SSH_BRIDGE_COMMAND", command, TRUE);

  if (fix && fix->allow_unknown)
    {
      env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS",
                              "true", TRUE);
    }

  knownhosts_file = fix ? fix->knownhosts_file : NULL;
  if (!knownhosts_file)
      knownhosts_file = SRCDIR "/src/ssh/mock_known_hosts";

  env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE",
                          knownhosts_file, TRUE);
  return env;
}

static CockpitTransport *
start_bridge (gchar **env,
              gchar **argv)
{
  GError *error = NULL;
  int fds[2];

  g_assert_cmpint (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds), ==, 0);
  g_spawn_async_with_pipes (BUILDDIR, argv, env,
                            G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH,
                            spawn_setup, GINT_TO_POINTER (fds[0]),
                            NULL, NULL, NULL, NULL, &error);
  g_assert_no_error (error);
  close (fds[0]);

  return cockpit_pipe_transport_new_fds ("test-ssh", fds[1], fds[1]);
}

static void
on_closed_set_flag (CockpitTransport *transport,
                    const gchar *problem,
                    gpointer user_data)
{
  gboolean *flag = user_data;
  g_assert_cmpstr (problem, ==, NULL);
  g_assert (*flag == FALSE);
  *flag = TRUE;
}

static void
setup (TestCase *tc,
       gconstpointer data)
{
  const TestFixture *fixture = data;
  const gchar *argv[] = { BUILDDIR "/cockpit-ssh", NULL, NULL };
  gchar **env = NULL;
  gchar *host = NULL;
  gchar *path = NULL;

  alarm (TIMEOUT);

  g_assert (fixture != NULL);

  env = setup_env (fixture);
  setup_mock_sshd (tc, data);

  if (tc->ssh_port)
    host = g_strdup_printf ("%s:%d", fixture->hostname ?: "127.0.0.1", tc->ssh_port);
  else
    host = g_strdup (fixture->hostname ?: "127.0.0.1");
  argv[1] = host;

  /* run our tests with temp home dir, to avoid influence from the real ~/.ssh */
  tc->home_dir = g_dir_make_tmp ("home.XXXXXX", NULL);
  g_assert (tc->home_dir != NULL);
  env = g_environ_setenv (env, "HOME", tc->home_dir, TRUE);
  /* use preload library to bend getpwuid_r home dir to the temporary one */
  env = g_environ_setenv (env, "LD_PRELOAD", BUILDDIR "/libpreload-temp-home.so", TRUE);

  tc->home_ssh_dir = g_build_filename (tc->home_dir, ".ssh", NULL);
  g_assert (tc->home_ssh_dir != NULL);

  if (fixture->knownhosts_home)
    {
      gchar *content;

      tc->home_knownhosts_file = g_build_filename (tc->home_ssh_dir, "known_hosts", NULL);
      g_assert (tc->home_knownhosts_file != NULL);
      g_assert_cmpint (mkdir (tc->home_ssh_dir, 0700), ==, 0);

      content = g_strdup_printf ("[%s]:%d %s\n",
                                 fixture->hostname ?: "127.0.0.1",
                                 (int)tc->ssh_port,
                                 fixture->knownhosts_home);

      g_assert (g_file_set_contents (tc->home_knownhosts_file, content, -1, NULL));

      g_free (content);
    }

#if LIBSSH_085
  if (fixture->test_home_ssh_config)
    {
      gchar *content;
      gchar **host_port;

      tc->home_ssh_config_file = g_build_filename (tc->home_ssh_dir, "config", NULL);
      if (!fixture->knownhosts_home)
          g_assert_cmpint (mkdir (tc->home_ssh_dir, 0700), ==, 0);
      host_port = g_strsplit (host, ":", 0);

      content = g_strdup_printf ("Host some_host\n "
                                 "\tHostname %s\n"
                                 "\tPort %s\n",
                                 host_port[0],
                                 g_strv_length (host_port) > 1 ? host_port[1] : "22");
      g_assert (g_file_set_contents (tc->home_ssh_config_file, content, -1, NULL));
      g_free (host);
      host = g_strdup ("some_host");
      argv[1] = host;

      g_strfreev (host_port);
      g_free (content);
    }
#endif

  if (fixture->knownhosts_sssd)
    {
      g_assert (fixture->knownhosts_sssd_host != NULL);
      env = g_environ_setenv (env, "COCKPIT_MOCK_SSS_KNOWN_HOSTS_HOST",
                              fixture->knownhosts_sssd_host, TRUE);
      env = g_environ_setenv (env, "COCKPIT_MOCK_SSS_KNOWN_HOSTS_KEY",
                              fixture->knownhosts_sssd, TRUE);

      path = g_strjoin (":",
                        SRCDIR "/src/ssh/mock-bin",
                        g_environ_getenv (env, "PATH") ?: "",
                        NULL);

      env = g_environ_setenv (env, "PATH", path, TRUE);
{'fraction_non_alphanumeric': 0.12364123159303882, 'fraction_numerical': 0.009210174029451138, 'mean_word_length': 2.669155206286837, 'pattern_counts': {'":': 4, '<': 23, '<?xml version=': 0, '>': 119, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 8, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2194088', 'n_tokens_mistral': 6806, 'n_tokens_neox': 6521, 'n_words': 1611}