dav/config.c

Sat, 20 Apr 2024 13:01:58 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 20 Apr 2024 13:01:58 +0200
changeset 815
1f40ca07ae1b
parent 806
673a803d2203
permissions
-rw-r--r--

add more xattr malloc checks

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2018 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 "main.h"
#include "pwd.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; 
}



/*
Repository* url2repo_s(cxstring url, char **path) {
    *path = NULL;
    
    int s;
    if(cx_strprefix(url, CX_STR("http://"))) {
        s = 7;
    } else if(cx_strprefix(url, CX_STR("https://"))) {
        s = 8;
    } else {
        s = 1;
    }

    // split URL into repository and path
    cxstring r = cx_strsubs(url, s);
    cxstring p = cx_strchr(r, '/');
    r = cx_strsubsl(url, 0, url.length-p.length);
    if(p.length == 0) {
        p = cx_strn("/", 1);
    }
    
    Repository *repo = get_repository(r);
    if(repo) {
        *path = cx_strdup(p).ptr;
    } else {
        // TODO: who is responsible for freeing this repository?
        // how can the callee know, if he has to call free()?
        repo = calloc(1, sizeof(Repository));
        repo->name = strdup("");
        repo->decrypt_content = true;
        repo->verification = true;
        repo->authmethods = CURLAUTH_BASIC;
        if(url.ptr[url.length-1] == '/') {
            repo->url = cx_strdup(url).ptr;
            *path = strdup("/");
        } else if (cx_strchr(url, '/').length > 0) {
            // TODO: fix the following workaround after
            //       fixing the inconsistent behavior of util_url_*()
            repo->url = util_url_base_s(url);
            cxmutstr truncated = cx_strdup(url);
            *path = strdup(util_url_path(truncated.ptr));
            free(truncated.ptr);
        } else {
            repo->url = cx_strdup(url).ptr;
            *path = strdup("/");
        }
    }
    
    return repo;
}

Repository* url2repo(const char *url, char **path) {
    return url2repo_s(cx_str(url), path);
}
*/

static int decrypt_secrets(CmdArgs *a, PwdStore *secrets) {
    if(cmd_getoption(a, "noinput")) {
        return 1;
    }
    
    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);
    }
    
    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(CmdArgs *a, char *credid, char **user, char **password) {
    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(CmdArgs *a, 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(a, 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, CmdArgs *a) {
    cxmutstr decodedpw = dav_repository_get_decodedpassword(repo);
    
    char *user = repo->user.value.ptr;
    char *password = decodedpw.ptr;
    
    if(!user && !password) {
        if(!get_stored_credentials(a, repo->stored_user.value.ptr, &user, &password)) {
            get_location_credentials(a, 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);
    // TODO: reactivate
    //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 || cmd_getoption(a, "insecure")) {
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYPEER, 0);
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYHOST, 0);
    }
    if(!cmd_getoption(a, "noinput")) {
        dav_session_set_authcallback(sn, authfunc, repo);
    }
    return sn;
}

int request_auth(DavSession *sn, void *userdata) {
    DavCfgRepository *repo = userdata;
    
    cxstring user = {NULL, 0};
    char ubuf[256];
    if(repo->user.value.ptr) {
       user = cx_strcast(repo->user.value);
    } else {
        fprintf(stderr, "User: ");
        fflush(stderr);
        user = cx_str(fgets(ubuf, 256, stdin));
    }
    if(!user.ptr) {
        return 0;
    }
    
    char *password = util_password_input("Password: ");
    if(!password || strlen(password) == 0) {
        return 0;
    }
    
    dav_session_set_auth_s(sn, user, cx_str(password));
    free(password);
    
    return 0;
}

mercurial