application/config.c

Wed, 31 Jan 2024 12:55:11 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 31 Jan 2024 12:55:11 +0100
changeset 17
7cfd36aa005b
parent 7
905ac52c910f
child 49
2f71f4ee247a
permissions
-rw-r--r--

add refresh button

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2024 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <cx/hash_map.h>
#include <cx/utils.h>
#include <errno.h>
#include <libxml/tree.h>

#include "pwd.h"
#include "config.h"
#include "system.h"

#include <libidav/utils.h>
#include <libidav/config.h>

#define xstreq(a,b) xmlStrEqual(BAD_CAST a, BAD_CAST b)
#define xstrEQ(a,b) !xmlStrcasecmp(BAD_CAST a, BAD_CAST b)

#define print_error(lineno, ...) \
    do {\
        fprintf(stderr, "Error (config.xml line %u): ", lineno); \
        fprintf(stderr, __VA_ARGS__); \
        fprintf(stderr, "Abort.\n"); \
    } while(0);
#define print_warning(lineno, ...) \
    do {\
        fprintf(stderr, "Warning (config.xml line %u): ", lineno); \
        fprintf(stderr, __VA_ARGS__); \
    } while(0);

#ifdef _WIN32
#define ENV_HOME getenv("USERPROFILE")
#else
#define ENV_HOME getenv("HOME")
#endif /* _WIN32 */

static CxMap* repos;
static CxMap* keys;

static DavConfig* davconfig;
static PwdStore* pstore;

static char* secretstore_unlock_cmd;
static char* secretstore_lock_cmd;

int check_config_dir(void) {
    char* file = util_concat_path(ENV_HOME, ".dav");
    int ret = 0;
    if (util_mkdir(file, S_IRWXU)) {
        if (errno != EEXIST) {
            ret = 1;
        }
    }
    free(file);
    return ret;
}

static DavContext* context;

void create_default_config(char* file) {
    xmlDoc* doc = xmlNewDoc(BAD_CAST "1.0");
    xmlNode* root = xmlNewNode(NULL, BAD_CAST "configuration");
    xmlDocSetRootElement(doc, root);
    xmlSaveFormatFileEnc(file, doc, "UTF-8", 1);
    xmlFreeDoc(doc);
}

char* config_file_path(char* name) {
    char* davd = util_concat_path(ENV_HOME, ".dav");
    if (!davd) {
        return NULL;
    }
    char* path = util_concat_path(davd, name);
    free(davd);
    return path;
}

cxmutstr config_load_file(const char* path) {
    FILE* file = sys_fopen(path, "r");
    if (!file) {
        return (cxmutstr) { NULL, 0 };
    }

    CxBuffer buf;
    cxBufferInit(&buf, NULL, 1024, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
    cx_stream_copy(file, &buf, (cx_read_func)fread, (cx_write_func)cxBufferWrite);
    fclose(file);

    return cx_mutstrn(buf.space, buf.size);
}

int load_config(DavContext* ctx) {
    context = ctx;
    // TODO: free the config somewhere
    repos = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 16);
    keys = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 16);

    char* pwfile = util_concat_path(ENV_HOME, ".dav/secrets.crypt");
    pstore = pwdstore_open(pwfile);
    free(pwfile);

    char* file = util_concat_path(ENV_HOME, ".dav/config.xml");

    struct stat s;
    if (stat(file, &s)) {
        switch (errno) {
        case ENOENT: {
            return 0;
        }
        default: {
            perror("Cannot load config.xml");
        }
        }
        return 1;
    }

    cxmutstr config_content = config_load_file(file);
    int config_error;
    davconfig = dav_config_load(config_content, &config_error);
    free(config_content.ptr);
    free(file);

    if (!davconfig) {
        fprintf(stderr, "Cannot load config.xml\n");
        return 1;
    }

    return dav_config_register_keys(davconfig, ctx, load_key_file);
}

DavConfig* get_config(void) {
    return davconfig;
}

int store_config(void) {
    if (check_config_dir()) {
        return 1;
    }

    CxBuffer* buf = dav_config2buf(davconfig);
    if (!buf) {
        return 1;
    }

    char* file = util_concat_path(ENV_HOME, ".dav/config.xml");
    FILE* cout = sys_fopen(file, "w");
    if (!cout) {
        cxBufferFree(buf);
        return 1;
    }

    // should only fail if we run out of disk space or something like that
    // in that case, the config file is only destroyed
    // could only be prevented, if we write to a temp file first and than
    // rename it
    fwrite(buf->space, buf->size, 1, cout);

    cxBufferFree(buf);
    fclose(cout);

    return 0;
}

void free_config(void) {
    if (davconfig) {
        dav_config_free(davconfig);
    }
}

cxmutstr load_key_file(const char* filename) {
    cxmutstr k;
    k.ptr = NULL;
    k.length = 0;

    FILE* file = NULL;
    if (filename[0] == '/') {
        file = sys_fopen(filename, "r");
    }
    else {
        char* path = util_concat_path(ENV_HOME, ".dav/");
        char* p2 = util_concat_path(path, filename);
        file = sys_fopen(p2, "r");
        free(path);
        free(p2);
    }

    if (!file) {
        fprintf(stderr, "Error: cannot load keyfile %s\n", filename);
        return k;
    }

    char* data = malloc(256);
    size_t r = fread(data, 1, 256, file);
    k.ptr = data;
    k.length = r;

    fclose(file);
    return k;
}

static char* get_attr_content(xmlNode* node) {
    // TODO: remove code duplication (util_xml_get_text)
    while (node) {
        if (node->type == XML_TEXT_NODE) {
            return (char*)node->content;
        }
        node = node->next;
    }
    return NULL;
}

int load_namespace(const xmlNode* node) {
    const char* prefix = NULL;
    const char* uri = NULL;

    xmlAttr* attr = node->properties;
    while (attr) {
        if (attr->type == XML_ATTRIBUTE_NODE) {
            char* value = get_attr_content(attr->children);
            if (!value) {
                print_error(
                    node->line,
                    "missing value for attribute %s\n", (char*)attr->name);
                return 1;
            }
            if (xstreq(attr->name, "prefix")) {
                prefix = value;
            }
            else if (xstreq(attr->name, "uri")) {
                uri = value;
            }
            else {
                print_error(
                    node->line,
                    "unexpected attribute %s\n", (char*)attr->name);
                return 1;
            }
        }
        attr = attr->next;
    }

    if (!prefix) {
        print_error(node->line, "missing prefix attribute\n");
        return 1;
    }
    if (!uri) {
        print_error(node->line, "missing uri attribute\n");
        return 1;
    }

    if (dav_get_namespace(context, prefix)) {
        print_error(node->line, "namespace prefix '%s' already used\n", prefix);
        return 1;
    }

    return dav_add_namespace(context, prefix, uri);
}

int load_secretstore(const xmlNode* node) {
    // currently only one secretstore is supported

    if (!pstore) {
        return 0;
    }

    node = node->children;
    int error = 0;
    while (node) {
        if (node->type == XML_ELEMENT_NODE) {
            char* value = util_xml_get_text(node);
            if (value) {
                if (xstreq(node->name, "unlock-command")) {
                    pstore->unlock_cmd = strdup(value);
                }
                else if (xstreq(node->name, "lock-command")) {
                    pstore->lock_cmd = strdup(value);
                }
            }
        }
        node = node->next;
    }

    return error;
}

PwdStore* get_pwdstore(void) {
    return pstore;
}

int pwdstore_save(PwdStore* pwdstore) {
    if (check_config_dir()) {
        return 1;
    }

    char* pwfile = util_concat_path(ENV_HOME, ".dav/secrets.crypt");
    int ret = pwdstore_store(pwdstore, pwfile);
    free(pwfile);
    return ret;
}



static int decrypt_secrets(PwdStore *secrets) {
    char *ps_password = NULL;
    if(secrets->unlock_cmd && strlen(secrets->unlock_cmd) > 0) {
        CxBuffer *cmd_out = cxBufferCreate(NULL, 128, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND);
        if(!util_exec_command(secrets->unlock_cmd, cmd_out)) {
            // command successful, get first line from output without newline
            // and use that as password for the secretstore
            size_t len = 0;
            for(size_t i=0;i<=cmd_out->size;i++) {
                if(i == cmd_out->size || cmd_out->space[i] == '\n') {
                    len = i;
                    break;
                }
            }
            if(len > 0) {
                ps_password = malloc(len + 1);
                memcpy(ps_password, cmd_out->space, len);
                ps_password[len] = 0;
            }
        }
        cxBufferFree(cmd_out);
    }

    return 1;
    // TODO
    /*
    if(!ps_password) {
        ps_password = util_password_input("Master password: ");
        if(!ps_password) {
            return 1;
        }
    }

    if(pwdstore_setpassword(secrets, ps_password)) {
        fprintf(stderr, "Error: cannot create key from password\n");
        return 1;
    }
    if(pwdstore_decrypt(secrets)) {
        fprintf(stderr, "Error: cannot decrypt secrets store\n");
        return 1;
    }
    return 0;
    */
}


typedef struct CredLocation {
    char *id;
    char *location;
} CredLocation;

static int cmp_url_cred_entry(CredLocation *e1, CredLocation *e2, void *n) {
    return strcmp(e2->location, e1->location);
}

static void free_cred_location(CredLocation *c) {
    // c->id is not a copy, therefore we don't have to free it
    free(c->location);
    free(c);
}

static int get_stored_credentials(char *credid, char **user, char **password) {
    return 0;
    // TODO
    /*
    if(!credid) {
        return 0;
    }
    PwdStore *secrets = get_pwdstore();
    if(!secrets) {
        fprintf(stderr, "Error: no secrets store available\n");
        return 0;
    }

    if(pwdstore_has_id(secrets, credid)) {
        if(!secrets->isdecrypted) {
            if(decrypt_secrets(a, secrets)) {
                return 0;
            }
        }

        PwdEntry *s_cred = pwdstore_get(secrets, credid);
        if(s_cred) {
            *user = s_cred->user;
            *password = s_cred->password;
            return 1;
        }
    } else {
        fprintf(stderr, "Error: credentials id '%s' not found\n", credid);
    }

    return 0;
    */
}


static int get_location_credentials(DavCfgRepository *repo, const char *path, char **user, char **password) {
    PwdStore *secrets = get_pwdstore();
    if(!secrets) {
        return 0;
    }

    /*
    * The list secrets->location contains urls or repo names as
    * location strings. We need a list, that contains only urls
    */
    CxList *locations = cxLinkedListCreate(cxDefaultAllocator, (cx_compare_func)cmp_url_cred_entry, CX_STORE_POINTERS);
    locations->simple_destructor = (cx_destructor_func)free_cred_location;
    CxIterator i = cxListIterator(secrets->locations);
    cx_foreach(PwdIndexEntry*, e, i) {
        CxIterator entry_iter = cxListIterator(e->locations);
        cx_foreach(char *, loc, entry_iter) {
            cxmutstr rpath;
            DavCfgRepository *r = dav_config_url2repo_s(davconfig, cx_str(loc), &rpath);
            CredLocation *urlentry = calloc(1, sizeof(CredLocation));
            urlentry->id = e->id;
            urlentry->location = util_concat_path_s(cx_strcast(r->url.value), cx_strcast(rpath)).ptr;
            cxListAdd(locations, urlentry);
            free(rpath.ptr);
        }
    }
    // the list must be sorted
    cxListSort(locations);

    // create full request url string and remove protocol prefix
    cxmutstr req_url_proto = util_concat_path_s(cx_strcast(repo->url.value), cx_str(path));
    cxstring req_url = cx_strcast(req_url_proto);
    if(cx_strprefix(req_url, CX_STR("http://"))) {
        req_url = cx_strsubs(req_url, 7);
    } else if(cx_strprefix(req_url, CX_STR("https://"))) {
        req_url = cx_strsubs(req_url, 8);
    }

    // iterate over sorted locations and check if a location is a prefix
    // of the requested url
    char *id = NULL;
    int ret = 0;
    i = cxListIterator(locations);
    cx_foreach(CredLocation*, cred, i) {
        cxstring cred_url = cx_str(cred->location);

        // remove protocol prefix
        if(cx_strprefix(cred_url, CX_STR("http://"))) {
            cred_url = cx_strsubs(cred_url, 7);
        } else if(cx_strprefix(cred_url, CX_STR("https://"))) {
            cred_url = cx_strsubs(cred_url, 8);
        }

        if(cx_strprefix(req_url, cred_url)) {
            id = cred->id;
            break;
        }
    }

    // if an id is found and we can access the decrypted secret store
    // we can set the user/password
    if(id && (secrets->isdecrypted || !decrypt_secrets(secrets))) {
        PwdEntry *cred = pwdstore_get(secrets, id);
        if(cred) {
            *user = cred->user;
            *password = cred->password;
            ret = 1;
        }
    }

    free(req_url_proto.ptr);
    cxListDestroy(locations);

    return ret;
}


DavSession* connect_to_repo(DavContext *ctx, DavCfgRepository *repo, const char *path, dav_auth_func authfunc) {
    cxmutstr decodedpw = dav_repository_get_decodedpassword(repo);

    char *user = repo->user.value.ptr;
    char *password = decodedpw.ptr;

    if(!user && !password) {
        if(!get_stored_credentials(repo->stored_user.value.ptr, &user, &password)) {
            get_location_credentials(repo, path, &user, &password);
        }
    }

    DavSession *sn = dav_session_new_auth(ctx, repo->url.value.ptr, user, password);
    if(password) {
        free(password);
    }

    sn->flags = dav_repository_get_flags(repo);
    sn->key = dav_context_get_key(ctx, repo->default_key.value.ptr);
    curl_easy_setopt(sn->handle, CURLOPT_HTTPAUTH, repo->authmethods);
    curl_easy_setopt(sn->handle, CURLOPT_SSLVERSION, repo->ssl_version);
    if(repo->cert.value.ptr) {
        curl_easy_setopt(sn->handle, CURLOPT_CAINFO, repo->cert.value.ptr);
    }
    if(!repo->verification.value) {
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYPEER, 0);
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYHOST, 0);
    }
    
    return sn;
}

mercurial