/**
 * xrdp: A Remote Desktop Protocol server.
 *
 * Copyright (C) Jay Sorg 2004-2013
 *
 * 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.
 */

/**
 *
 * @file sesrun.c
 * @brief An utility to start a session
 * @author Jay Sorg, Simone Fedele
 *
 */

#if defined(HAVE_CONFIG_H)
#include <config_ac.h>
#endif

#include <unistd.h>
#include <limits.h>
#include <ctype.h>

#include "parse.h"
#include "trans.h"
#include "os_calls.h"
#include "sesman_config.h"
#include "log.h"
#include "string_calls.h"
#include "guid.h"

#include "tools_common.h"

// cppcheck doesn't always set this macro to something in double-quotes
#if defined(__cppcheck__)
#undef PACKAGE_VERSION
#endif

#if !defined(PACKAGE_VERSION)
#define PACKAGE_VERSION "???"
#endif

#ifndef MAX_PASSWORD_LEN
#   define MAX_PASSWORD_LEN 512
#endif

#ifndef DEFAULT_WIDTH
#   define DEFAULT_WIDTH 1280
#endif

#ifndef DEFAULT_HEIGHT
#   define DEFAULT_HEIGHT 1024
#endif

/* Default setting used by Windows 10 mstsc.exe */
#ifndef DEFAULT_BPP
#   define DEFAULT_BPP 32
#endif

#ifndef DEFAULT_SESSION_TYPE
#   define DEFAULT_SESSION_TYPE "Xorg"
#endif

/**
 * Maps session type strings session type codes
 */
static struct
{
    const char *name;
    enum scp_session_type type;
} type_map[] =
{
    { "Xvnc", SCP_SESSION_TYPE_XVNC},
    { "Xorg", SCP_SESSION_TYPE_XORG},
    { NULL, (enum scp_session_type) - 1}
};

/**
 * Parameters needed for a session
 */
struct session_params
{
    int width;
    int height;
    int bpp;
    enum scp_session_type session_type;

    const char *directory;
    const char *shell;
    const char *ip_addr;

    const char *username;
    char password[MAX_PASSWORD_LEN + 1];
};

/**************************************************************************//**
 * Maps a string to a session type value
 *
 * @param string session type string
 * @param[out] value session type value
 * @return 0 for success or != 0 if not found
 */
static
int string_to_session_type(const char *t, enum scp_session_type *value)
{
    unsigned int i;
    for (i = 0 ; type_map[i].name != NULL; ++i)
    {
        if (g_strcasecmp(type_map[i].name, t) == 0)
        {
            *value = type_map[i].type;
            return 0;
        }
    }

    return 1;
}

/**************************************************************************//**
 * Returns a list of supported session types
 *
 * Caller supplies a buffer. Buffer handling and buffer overflow detection are
 * the same as snprint()
 *
 * @param buff area for result
 * @param bufflen Size of result
 * @return number of characters for the output string
 */
static
unsigned int get_session_type_list(char *buff, unsigned int bufflen)
{
    unsigned int i;
    unsigned int ret = 0;
    const char *sep = "";

    for (i = 0 ; type_map[i].name != NULL; ++i)
    {
        if (ret < bufflen)
        {
            ret += g_snprintf(buff + ret, bufflen - ret,
                              "%s%s", sep, type_map[i].name);
            sep = ", ";
        }
    }

    return ret;
}

/**************************************************************************//**
 * Prints a brief summary of options and defaults
 */
static void
usage(void)
{
    char sesstype_list[64];

    (void)get_session_type_list(sesstype_list, sizeof(sesstype_list));

    g_printf("xrdp session starter v" PACKAGE_VERSION "\n");
    g_printf("\nusage:\n");
    g_printf("sesrun --help\n"
             "\nor\n"
             "sesrun [options] [username]\n\n");
    g_printf("options:\n");
    g_printf("    -g <geometry>         Default:%dx%d\n",
             DEFAULT_WIDTH, DEFAULT_HEIGHT);
    g_printf("    -b <bits-per-pixel>   Default:%d\n", DEFAULT_BPP);
    g_printf("    -t <type>             Default:%s\n", DEFAULT_SESSION_TYPE);
    g_printf("    -D <directory>        Default: $HOME\n"
             "    -S <shell>            Default: Defined window manager\n"
             "    -p <password>         TESTING ONLY - DO NOT USE IN PRODUCTION\n"
             "    -F <file-descriptor>  Read password from this file descriptor\n"
             "    -c <sesman_ini>       Alternative sesman.ini file\n");
    g_printf("\nSupported types are %s\n",
             sesstype_list);
    g_printf("\nIf username is omitted, the current user is used.\n"
             "If username is provided, password is needed.\n"
             "    Password is prompted for if -p or -F are not specified\n");
}


/**************************************************************************//**
 * Parses a string <width>x<height>
 *
 * @param geom_str Input string
 * @param sp Session parameter structure for resulting width and height
 * @return !=0 for success
 */
static int
parse_geometry_string(const char *geom_str, struct session_params *sp)
{
    int result = 0;
    unsigned int sep_count = 0; /* Count of 'x' separators */
    unsigned int other_count = 0; /* Count of non-digits and non separators */
    const char *sepp = NULL; /* Pointer to the 'x' */
    const char *p = geom_str;

    while (*p != '\0')
    {
        if (!isdigit(*p))
        {
            if (*p == 'x' || *p == 'X')
            {
                ++sep_count;
                sepp = p;
            }
            else
            {
                ++other_count;
            }
        }
        ++p;
    }

    if (sep_count != 1 || other_count > 0 ||
            sepp == geom_str ||  /* Separator at start of string */
            sepp == (p - 1) )    /* Separator at end of string */
    {
        LOG(LOG_LEVEL_ERROR, "Invalid geometry string '%s'", geom_str);
    }
    else
    {
        sp->width = atoi(geom_str);
        sp->height = atoi(sepp + 1);
        result = 1;
    }

    return result;
}


/**************************************************************************//**
 * Read a password from a file descriptor
 *
 * @param fd_str string representing file descriptor
 * @param sp Session parameter structure for resulting password
 * @return !=0 for success
 */
static int
read_password_from_fd(const char *fd_str, struct session_params *sp)
{
    int result = 0;
    int s = g_file_read(atoi(fd_str), sp->password, sizeof (sp->password) - 1);
    if (s < 0)
    {
        LOG(LOG_LEVEL_ERROR, "Can't read password from fd %s - %s",
            fd_str, g_get_strerror());
        sp->password[0] = '\0';
    }
    else
    {
        sp->password[s] = '\0';
        if (s > 0 && sp->password[s - 1] == '\n')
        {
            sp->password[s - 1] = '\0';
        }
        result = 1;
    }
    return result;
}

/**************************************************************************//**
 * Parses the program args
 *
 * @param argc Passed to main
 * @param @argv Passed to main
 * @param sp Session parameter structure for resulting values
 * @param sesman_ini Pointer to an alternative config file if one is specified
 * @return !=0 for success
 */
static int
parse_program_args(int argc, char *argv[], struct session_params *sp,
                   const char **sesman_ini)
{
    int params_ok = 1;
    int opt;
    bool_t password_set = 0;

    sp->width = DEFAULT_WIDTH;
    sp->height = DEFAULT_HEIGHT;
    sp->bpp = DEFAULT_BPP;
    (void)string_to_session_type(DEFAULT_SESSION_TYPE, &sp->session_type);

    sp->directory = "";
    sp->shell = "";
    sp->ip_addr = "";

    sp->username = NULL;
    sp->password[0] = '\0';

    while ((opt = getopt(argc, argv, "g:b:s:t:D:S:p:F:c:")) != -1)
    {
        switch (opt)
        {
            case 'g':
                if (!parse_geometry_string(optarg, sp))
                {
                    params_ok = 0;
                }
                break;

            case 'b':
                sp->bpp = atoi(optarg);
                break;

            case 't':
                if (string_to_session_type(optarg, &sp->session_type) != 0)
                {
                    LOG(LOG_LEVEL_ERROR, "Unrecognised session type '%s'",
                        optarg);
                    params_ok = 0;
                }
                break;

            case 'D':
                sp->directory = optarg;
                break;

            case 'S':
                sp->shell = optarg;
                break;

            case 'p':
                if (password_set)
                {
                    LOG(LOG_LEVEL_WARNING,
                        "Ignoring option '%c' - password already set ",
                        (char)opt);
                }
                else
                {
                    g_strncpy(sp->password, optarg, sizeof(sp->password) - 1);
                    password_set = 1;
                }
                break;

            case 'F':
                if (password_set)
                {
                    LOG(LOG_LEVEL_WARNING,
                        "Ignoring option '%c' - password already set ",
                        (char)opt);
                }
                else
                {
                    if (read_password_from_fd(optarg, sp))
                    {
                        password_set = 1;
                    }
                    else
                    {
                        params_ok = 0;
                    }
                }
                break;

            case 'c':
                *sesman_ini = optarg;
                break;

            default:
                LOG(LOG_LEVEL_ERROR, "Unrecognised switch '%c'", (char)opt);
                params_ok = 0;
        }
    }

    if (argc == optind)
    {
        // No username was specified
        if (password_set)
        {
            LOG(LOG_LEVEL_WARNING, "No username - ignoring specified password");
            sp->password[0] = '\0';
        }
        sp->username = NULL;
    }
    else if ((argc - optind) > 1)
    {
        LOG(LOG_LEVEL_ERROR, "Unexpected arguments after username");
        params_ok = 0;
    }
    else if (params_ok)
    {
        // A username is specified
        sp->username = argv[optind];
        if (!password_set)
        {
            const char *p = getpass("Password: ");
            if (p == NULL)
            {
                params_ok = 0;
            }
            else
            {
                g_snprintf(sp->password, sizeof(sp->password), "%s", p);
            }
        }
    }

    return params_ok;
}

/**************************************************************************//**
 * Sends an SCP login request
 *
 * A sys login request (i.e. username / password) is used if a username
 * is specified. Otherwise we use a uds login request for the current user.
 *
 * @param t SCP connection
 * @param sp Data for request
 */
static int
send_login_request(struct trans *t, const struct session_params *sp)
{
    int rv;
    LOG(LOG_LEVEL_DEBUG, "ip_addr:\"%s\"", sp->ip_addr);
    if (sp->username != NULL)
    {
        /* Only log the password in development builds */
        LOG_DEVEL(LOG_LEVEL_DEBUG, "password:\"%s\"", sp->password);

        rv = scp_send_sys_login_request(t, sp->username,
                                        sp->password, sp->ip_addr);
    }
    else
    {
        rv = scp_send_uds_login_request(t);
    }

    return rv;
}

/**************************************************************************//**
 * Receives an SCP login response
 *
 * @param t SCP transport to receive reply on
 * @param[out] server_closed != 0 if server has gone away
 * @return 0 for successful authentication
 */
static int
handle_login_response(struct trans *t, int *server_closed)
{
    enum scp_login_status login_result;

    int rv = wait_for_sesman_reply(t, E_SCP_LOGIN_RESPONSE);
    if (rv != 0)
    {
        *server_closed = 1;
    }
    else
    {
        rv = scp_get_login_response(t, &login_result, server_closed, NULL);
        if (rv == 0)
        {
            if (login_result != E_SCP_LOGIN_OK)
            {
                char msg[256];
                scp_login_status_to_str(login_result, msg, sizeof(msg));
                g_printf("Login failed; %s\n", msg);
                rv = 1;
            }
        }
        scp_msg_in_reset(t); // Done with this message
    }

    return rv;
}


/**************************************************************************//**
 * Sends an SCP create session request
 *
 * @param t SCP connection
 * @param sp Data for request
 */
static int
send_create_session_request(struct trans *t, const struct session_params *sp)
{
    LOG(LOG_LEVEL_DEBUG,
        "width:%d  height:%d  bpp:%d  code:%d\n"
        "directory:\"%s\" shell:\"%s\"",
        sp->width, sp->height, sp->bpp, sp->session_type,
        sp->directory, sp->shell);

    return scp_send_create_session_request(
               t, sp->session_type,
               sp->width, sp->height, sp->bpp, sp->shell, sp->directory);
}

/**************************************************************************//**
 * Receives an SCP create session response
 *
 * @param t SCP transport to receive reply on
 * @return 0 for success
 */
static int
handle_create_session_response(struct trans *t)
{
    enum scp_screate_status status;
    int display;
    struct guid guid;

    int rv = wait_for_sesman_reply(t, E_SCP_CREATE_SESSION_RESPONSE);
    if (rv == 0)
    {
        rv = scp_get_create_session_response(t, &status,
                                             &display, &guid);

        if (rv == 0)
        {
            if (status != E_SCP_SCREATE_OK)
            {
                char msg[256];
                scp_screate_status_to_str(status, msg, sizeof(msg));
                g_printf("Connection failed; %s\n", msg);
                rv = 1;
            }
            else
            {
                char guid_str[GUID_STR_SIZE];
                g_printf("ok display=:%d GUID=%s\n",
                         display,
                         guid_to_str(&guid, guid_str));
            }
        }
        scp_msg_in_reset(t); // Done with this message
    }

    return rv;
}

/******************************************************************************/
int
main(int argc, char **argv)
{
    const char *sesman_ini = XRDP_CFG_PATH "/sesman.ini";
    struct config_sesman *cfg = NULL;

    struct trans *t = NULL;
    struct session_params sp;

    struct log_config *logging;
    int rv = 1;

    logging = log_config_init_for_console(LOG_LEVEL_WARNING,
                                          g_getenv("SESRUN_LOG_LEVEL"));
    log_start_from_param(logging);
    log_config_free(logging);

    if (argc == 2 && g_strcmp(argv[1], "--help") == 0)
    {
        usage();
        rv = 0;
    }
    else if (!parse_program_args(argc, argv, &sp, &sesman_ini))
    {
        usage();
    }
    else if ((cfg = config_read(sesman_ini)) == NULL)
    {
        LOG(LOG_LEVEL_ERROR, "error reading config file %s : %s",
            sesman_ini, g_get_strerror());
    }
    else if (!(t = scp_connect(cfg->listen_port, "xrdp-sesrun", NULL)))
    {
        LOG(LOG_LEVEL_ERROR, "connect error - %s", g_get_strerror());
    }
    else
    {
        int server_closed = 0;
        while (!server_closed)
        {
            rv = send_login_request(t, &sp);
            if (rv != 0)
            {
                LOG(LOG_LEVEL_ERROR, "Error sending login request to sesman");
                break;
            }

            rv = handle_login_response(t, &server_closed);
            if (rv == 0)
            {
                break; /* Successful authentication */
            }
            if (!server_closed)
            {
                const char *p = getpass("Password: ");
                if (p == NULL)
                {
                    break;
                }
                g_snprintf(sp.password, sizeof(sp.password), "%s", p);
            }
        }

        if (rv == 0)
        {
            if ((rv = send_create_session_request(t, &sp)) == 0)
            {
                rv = handle_create_session_response(t);
            }
        }
        trans_delete(t);
    }

    g_memset(sp.password, '\0', sizeof(sp.password));
    config_free(cfg);
    log_end();

    return rv;
}
