#define _POSIX_C_SOURCE 200112
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#define RET_OK 0
#define RET_ERR 1

struct http_response
{
    char* data;
    size_t length;
};

static size_t write_data(void *data, size_t size, size_t nitems, void* user_data)
{
    size_t total_size = size * nitems;
	struct http_response *response = (struct http_response*)user_data;

	response->data = (char*)realloc(response->data, response->length + total_size + 1);
	if (!response->data) {
        syslog(LOG_CRIT, "ran out of memory while reallocating http response structure!");
        return 0;
	}

	memcpy(response->data + response->length, data, total_size);
	response->length += total_size;
	response->data[response->length] = 0;

	return total_size;
}

char* get_post_body(const char* user, const char* hostname)
{
    const char* json_format = "{\"origin\": \"%s\", \"user\": \"%s\"}";
    int sz = snprintf(NULL, 0, json_format, hostname, user) + 1;
    char* json = (char*)malloc(sz);
    int res = snprintf(json, sz, json_format, hostname, user);
    if (res < 0)
    {
        perror("snprintf");
        free(json);
        return NULL;
    }

    return json;
}

int request_auth(pam_handle_t *pamh, char* post_body, struct http_response* resp)
{
    const char url_request[]   = "https://wnt.segfault.party/request_auth";
    struct curl_slist* header_list = curl_slist_append(NULL, "Content-Type: application/json");
    CURL* curl_handle = NULL;
    CURLcode res;
    int retval = RET_ERR;

    if (!(curl_handle = curl_easy_init()))
        goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_URL, url_request);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "pam_telegram/0.1");
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_data);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_POST, 1L);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, post_body);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, header_list);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void*)resp);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_perform(curl_handle);
    if (res != CURLE_OK)
    {
        pam_syslog(pamh, LOG_ERR, "curl_easy_perform failed: %s", curl_easy_strerror(res));
        goto cleanup;
    }

    retval = RET_OK;

cleanup:
    if (curl_handle) curl_easy_cleanup(curl_handle);
    if (header_list) curl_slist_free_all(header_list);

    return retval;
}

int poll_authorization_status(pam_handle_t *pamh, char* poll_url, struct http_response *resp)
{
    CURL* curl_handle = NULL;
    CURLcode res;
    int retval = RET_ERR;

    if (!(curl_handle = curl_easy_init()))
        goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_URL, poll_url);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "pam_telegram/0.1");
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_data);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void*)resp);
    if (res != CURLE_OK) goto cleanup;

    res = curl_easy_perform(curl_handle);
    if (res != CURLE_OK)
    {
        pam_syslog(pamh, LOG_ERR, "curl_easy_perform failed: %s", curl_easy_strerror(res));
        goto cleanup;
    }

    retval = RET_OK;

cleanup:
    if (curl_handle) curl_easy_cleanup(curl_handle);

    return retval;

}

int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    int pam_retval = PAM_AUTH_ERR;
    int res;
    int sz;

    const char url_poll_base[] = "https://wnt.segfault.party/get_auth_status";
    char* url_poll = NULL;

    struct http_response id_resp = {0};
    struct http_response poll_resp = {0};

    const char *user;
    char hostname[HOST_NAME_MAX] = "";

    char* json = NULL;

    openlog(NULL, LOG_PID, LOG_AUTHPRIV);

    pam_info(pamh, "Waiting for Telegram authorization...");

    gethostname(hostname, HOST_NAME_MAX);
    pam_get_user(pamh, &user, NULL);

    json = get_post_body(user, hostname);

    if (!json)
        goto cleanup;


    // if any of these fail, fail authentication.
    pam_retval = PAM_AUTH_ERR;

    res = request_auth(pamh, json, &id_resp);

    if (res != RET_OK)
        goto cleanup;

    // We've got a request id to poll.

    pam_syslog(pamh, LOG_INFO, "request_auth returned request id %s, starting polling...", id_resp.data);

    pam_info(pamh, "Please authorize the login request with id `%s'", id_resp.data);

    sz = snprintf(NULL, 0, "%s?request_id=%s", url_poll_base, id_resp.data) + 1;
    url_poll = (char*) malloc(sz);
    res = snprintf(url_poll, sz, "%s?request_id=%s", url_poll_base, id_resp.data);

    while (1)
    {
        res = poll_authorization_status(pamh, url_poll, &poll_resp);
        if (res != RET_OK)
            break;

        if (strcmp(poll_resp.data, "authorized") == 0)
        {
            pam_retval = PAM_SUCCESS;
            break;
        }
        else if (strcmp(poll_resp.data, "unauthorized") == 0)
        {
            pam_retval = PAM_AUTH_ERR;
            break;
        }
        // else continue polling
        free(poll_resp.data);
        poll_resp = (struct http_response){};
        sleep(2);
    }

    pam_syslog(pamh, LOG_INFO, "polling returned result: %s", poll_resp.data);


cleanup:
    if (json) free(json);
    if (url_poll) free(url_poll);
    if (poll_resp.data) free(poll_resp.data);
    if (id_resp.data) free(id_resp.data);

    return pam_retval;
}

int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    // there are no credentials to speak of, per se
    pam_syslog(pamh, LOG_INFO, "pam_sm_setcred called.");
    return PAM_SUCCESS;
}
