Wed, 31 Jan 2024 12:55:11 +0100
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; }