diff -r 2483f517c562 -r b5bb7b3cd597 libidav/session.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libidav/session.c Mon Jan 22 17:27:47 2024 +0100 @@ -0,0 +1,622 @@ +/* + * 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 +#include +#include + +#include +#include +#include +#include + +#include "utils.h" +#include "session.h" +#include "resource.h" +#include "methods.h" + +DavSession* dav_session_new(DavContext *context, char *base_url) { + if(!base_url) { + return NULL; + } + cxstring url = cx_str(base_url); + if(url.length == 0) { + return NULL; + } + DavSession *sn = malloc(sizeof(DavSession)); + memset(sn, 0, sizeof(DavSession)); + sn->mp = cxMempoolCreate(DAV_SESSION_MEMPOOL_SIZE, NULL); + sn->pathcache = cxHashMapCreate(sn->mp->allocator, CX_STORE_POINTERS, DAV_PATH_CACHE_SIZE); + sn->key = NULL; + sn->errorstr = NULL; + sn->error = DAV_OK; + sn->flags = 0; + + dav_session_set_baseurl(sn, base_url); + + sn->handle = curl_easy_init(); + curl_easy_setopt(sn->handle, CURLOPT_FOLLOWLOCATION, 1L); + + // lock manager is created on-demand + sn->locks = NULL; + + // set proxy + DavProxy *proxy = cx_strprefix(url, CX_STR("https")) ? context->https_proxy + : context->http_proxy; + + if (proxy->url) { + curl_easy_setopt(sn->handle, CURLOPT_PROXY, proxy->url); + if (proxy->username) { + curl_easy_setopt(sn->handle, CURLOPT_PROXYUSERNAME, + proxy->username); + if (proxy->password) { + curl_easy_setopt(sn->handle, CURLOPT_PROXYPASSWORD, + proxy->password); + } else { + // TODO: prompt + } + } + if(proxy->no_proxy) { + curl_easy_setopt(sn->handle, CURLOPT_NOPROXY, + proxy->no_proxy); + } + } + + // set url +#if LIBCURL_VERSION_NUM >= 0x072D00 + curl_easy_setopt(sn->handle, CURLOPT_DEFAULT_PROTOCOL, "http"); +#endif + curl_easy_setopt(sn->handle, CURLOPT_URL, base_url); + + // add to context + cxListAdd(context->sessions, sn); + sn->context = context; + + return sn; +} + +DavSession* dav_session_new_auth( + DavContext *context, + char *base_url, + char *user, + char *password) +{ + DavSession *sn = dav_session_new(context, base_url); + if(!sn) { + return NULL; + } + dav_session_set_auth(sn, user, password); + return sn; +} + +void dav_session_set_auth(DavSession *sn, char *user, char *password) { + if(user && password) { + size_t ulen = strlen(user); + size_t plen = strlen(password); + size_t upwdlen = ulen + plen + 2; + char *upwdbuf = malloc(upwdlen); + snprintf(upwdbuf, upwdlen, "%s:%s", user, password); + curl_easy_setopt(sn->handle, CURLOPT_USERPWD, upwdbuf); + free(upwdbuf); + } +} + +void dav_session_set_baseurl(DavSession *sn, char *base_url) { + const CxAllocator *a = sn->mp->allocator; + if(sn->base_url) { + cxFree(a, sn->base_url); + } + + cxstring url = cx_str(base_url); + if(url.ptr[url.length - 1] == '/') { + cxmutstr url_m = cx_strdup_a(a, cx_str(base_url)); + sn->base_url = url_m.ptr; + } else { + char *url_str = cxMalloc(a, url.length + 2); + memcpy(url_str, base_url, url.length); + url_str[url.length] = '/'; + url_str[url.length + 1] = '\0'; + sn->base_url = url_str; + } +} + +void dav_session_enable_encryption(DavSession *sn, DavKey *key, int flags) { + sn->key = key; + // TODO: review sanity + if(flags != 0) { + sn->flags |= flags; + } else { + sn->flags |= DAV_SESSION_ENCRYPT_CONTENT; + } +} + +void dav_session_set_authcallback(DavSession *sn, dav_auth_func func, void *userdata) { + sn->auth_prompt = func; + sn->authprompt_userdata = userdata; +} + +void dav_session_set_progresscallback(DavSession *sn, dav_progress_func get, dav_progress_func put, void *userdata) { + sn->get_progress = get; + sn->put_progress = put; + sn->progress_userdata = userdata; +} + +CURLcode dav_session_curl_perform(DavSession *sn, long *status) { + return dav_session_curl_perform_buf(sn, NULL, NULL, status); +} + +CURLcode dav_session_curl_perform_buf(DavSession *sn, CxBuffer *request, CxBuffer *response, long *status) { + CURLcode ret = curl_easy_perform(sn->handle); + long http_status; + curl_easy_getinfo(sn->handle, CURLINFO_RESPONSE_CODE, &http_status); + if(ret == CURLE_OK) { + if(sn->logfunc) { + char *log_method; + char *log_url; + curl_easy_getinfo(sn->handle, CURLINFO_EFFECTIVE_URL, &log_url); + curl_easy_getinfo(sn->handle, CURLINFO_EFFECTIVE_METHOD , &log_method); + char *log_reqbody = NULL; + size_t log_reqbodylen = 0; + char *log_rpbody = NULL; + size_t log_rpbodylen = 0; + if(request) { + log_reqbody = request->space; + log_reqbodylen = request->size; + } + if(response) { + log_rpbody = response->space; + log_rpbodylen = response->size; + } + sn->logfunc(sn, log_method, log_url, log_reqbody, log_reqbodylen, http_status, log_rpbody, log_rpbodylen); + } + + if(http_status == 401 && sn->auth_prompt) { + if(!sn->auth_prompt(sn, sn->authprompt_userdata)) { + if(request) { + cxBufferSeek(request, 0, SEEK_SET); + } + if(response) { + cxBufferSeek(response, 0, SEEK_SET); + } + ret = curl_easy_perform(sn->handle); + curl_easy_getinfo(sn->handle, CURLINFO_RESPONSE_CODE, &http_status); + } + } + + } + + if(status) { + *status = http_status; + } + return ret; +} + +int dav_session_get_progress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + DavResource *res = clientp; + DavSession *sn = res->session; + if(sn->get_progress) { + sn->get_progress(res, (int64_t)dltotal, (int64_t)dlnow, sn->progress_userdata); + } + return 0; +} + +int dav_session_put_progress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + DavResource *res = clientp; + DavSession *sn = res->session; + if(sn->put_progress) { + sn->put_progress(res, (int64_t)ultotal, (int64_t)ulnow, sn->progress_userdata); + } + return 0; +} + +void dav_session_set_error(DavSession *sn, CURLcode c, int status) { + if(status > 0) { + switch(status) { + default: { + switch(c) { + default: sn->error = DAV_ERROR; + } + break; + } + case 401: sn->error = DAV_UNAUTHORIZED; break; + case 403: sn->error = DAV_FORBIDDEN; break; + case 404: sn->error = DAV_NOT_FOUND; break; + case 405: sn->error = DAV_METHOD_NOT_ALLOWED; break; + case 407: sn->error = DAV_PROXY_AUTH_REQUIRED; break; + case 409: sn->error = DAV_CONFLICT; break; + case 412: sn->error = DAV_PRECONDITION_FAILED; break; + case 413: sn->error = DAV_REQUEST_ENTITY_TOO_LARGE; break; + case 414: sn->error = DAV_REQUEST_URL_TOO_LONG; break; + case 423: sn->error = DAV_LOCKED; break; + case 511: sn->error = DAV_NET_AUTH_REQUIRED; break; + } + } else { + switch(c) { + case CURLE_UNSUPPORTED_PROTOCOL: sn->error = DAV_UNSUPPORTED_PROTOCOL; break; + case CURLE_COULDNT_RESOLVE_PROXY: sn->error = DAV_COULDNT_RESOLVE_PROXY; break; + case CURLE_COULDNT_RESOLVE_HOST: sn->error = DAV_COULDNT_RESOLVE_HOST; break; + case CURLE_COULDNT_CONNECT: sn->error = DAV_COULDNT_CONNECT; break; + case CURLE_OPERATION_TIMEDOUT: sn->error = DAV_TIMEOUT; break; + case CURLE_SSL_CONNECT_ERROR: + case CURLE_PEER_FAILED_VERIFICATION: + case CURLE_SSL_ENGINE_NOTFOUND: + case CURLE_SSL_ENGINE_SETFAILED: + case CURLE_SSL_CERTPROBLEM: + case CURLE_SSL_CIPHER: +//#ifndef CURLE_SSL_CACERT +// case CURLE_SSL_CACERT: +//#endif + case CURLE_SSL_CACERT_BADFILE: + case CURLE_SSL_SHUTDOWN_FAILED: + case CURLE_SSL_CRL_BADFILE: + case CURLE_SSL_ISSUER_ERROR: sn->error = DAV_SSL_ERROR; break; + default: sn->error = DAV_ERROR; break; + } + } + if(c != CURLE_OK) { + dav_session_set_errstr(sn, curl_easy_strerror(c)); + } else { + dav_session_set_errstr(sn, NULL); + } +} + +void dav_session_set_errstr(DavSession *sn, const char *str) { + if(sn->errorstr) { + dav_session_free(sn, sn->errorstr); + } + char *errstr = NULL; + if(str) { + errstr = dav_session_strdup(sn, str); + } + sn->errorstr = errstr; +} + +void dav_session_destroy(DavSession *sn) { + // remove session from context + CxList *sessions = sn->context->sessions; + ssize_t i = cxListFind(sessions, sn); + if(i >= 0) { + cxListRemove(sessions, i); + } else { + printf("Error: session not found in ctx->sessions\n"); + dav_session_destructor(sn); + } +} + +void dav_session_destructor(DavSession *sn) { + cxMempoolDestroy(sn->mp); + curl_easy_cleanup(sn->handle); + free(sn); +} + + +void* dav_session_malloc(DavSession *sn, size_t size) { + return cxMalloc(sn->mp->allocator, size); +} + +void* dav_session_calloc(DavSession *sn, size_t nelm, size_t size) { + return cxCalloc(sn->mp->allocator, nelm, size); +} + +void* dav_session_realloc(DavSession *sn, void *ptr, size_t size) { + return cxRealloc(sn->mp->allocator, ptr, size); +} + +void dav_session_free(DavSession *sn, void *ptr) { + cxFree(sn->mp->allocator, ptr); +} + +char* dav_session_strdup(DavSession *sn, const char *str) { + return cx_strdup_a(sn->mp->allocator, cx_str((char*)str)).ptr; +} + + +char* dav_session_create_plain_href(DavSession *sn, const char *path) { + if(!DAV_ENCRYPT_NAME(sn) && !DAV_DECRYPT_NAME(sn)) { + // non encrypted file names + char *url = util_path_to_url(sn, path); + char *href = dav_session_strdup(sn, util_url_path(url)); + free(url); + return href; + } else { + return NULL; + } +} + +char* dav_session_get_href(DavSession *sn, const char *path) { + if(DAV_DECRYPT_NAME(sn) || DAV_ENCRYPT_NAME(sn)) { + cxstring p = cx_str(path); + CxBuffer href; + CxBuffer pbuf; + cxBufferInit(&href, NULL, 256, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND); + cxBufferInit(&pbuf, NULL, 256, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND); + + int start = 0; + int begin = 0; + + // check path cache + char *cp = strdup(path); + //printf("cp: %s\n", cp); + while(strlen(cp) > 1) { + char *cached = cxMapGet(sn->pathcache, cx_hash_key_str(cp)); + if(cached) { + start = strlen(cp); + begin = start; + cxBufferPutString(&href, cached); + break; + } else { + // check, if the parent path is cached + char *f = cp; + cp = util_parent_path(cp); + free(f); + } + } + free(cp); + if(href.pos == 0) { + // if there are no cached elements we have to add the base url path + // to the href buffer + cxBufferPutString(&href, util_url_path(sn->base_url)); + } + + // create resource for name lookup + cxmutstr rp = cx_strdup(cx_strn(path, start)); + DavResource *root = dav_resource_new(sn, rp.ptr); + free(rp.ptr); + resource_set_href(root, cx_strn(href.space, href.pos)); + + // create request buffer for propfind requests + CxBuffer *rqbuf = create_basic_propfind_request(); + + cxstring remaining = cx_strsubs(p, start); + CxStrtokCtx elms = cx_strtok(remaining, CX_STR("/"), INT_MAX); + DavResource *res = root; + cxBufferPutString(&pbuf, res->path); + // iterate over all remaining path elements + cxstring elm; + while(cx_strtok_next(&elms, &elm)) { + if(elm.length > 0) { + //printf("elm: %.*s\n", elm.length, elm.ptr); + DavResource *child = dav_find_child(sn, res, rqbuf, elm.ptr); + + // if necessary add a path separator + if(pbuf.space[pbuf.pos-1] != '/') { + if(href.space[href.pos-1] != '/') { + cxBufferPut(&href, '/'); + } + cxBufferPut(&pbuf, '/'); + } + // add last path/href to the cache + cxstring pp = cx_strn(pbuf.space, pbuf.size); + cxstring hh = cx_strn(href.space, href.size); + dav_session_cache_path(sn, pp, hh); + + cxBufferWrite(elm.ptr, 1, elm.length, &pbuf); + if(child) { + // href is already URL encoded, so don't encode again + cxBufferPutString(&href, util_resource_name(child->href)); + res = child; + } else if(DAV_ENCRYPT_NAME(sn)) { + char *random_name = util_random_str(); + cxBufferPutString(&href, random_name); + free(random_name); + } else { + // path is not URL encoded, so we have to do this here + cxstring resname = cx_str(util_resource_name((const char*)path)); + // the name of collections ends with + // a trailing slash, which MUST NOT be encoded + if(resname.ptr[resname.length-1] == '/') { + char *esc = curl_easy_escape(sn->handle, + resname.ptr, resname.length-1); + cxBufferWrite(esc, 1, strlen(esc), &href); + cxBufferPut(&href, '/'); + curl_free(esc); + } else { + char *esc = curl_easy_escape(sn->handle, + resname.ptr, resname.length); + cxBufferWrite(esc, 1, strlen(esc), &href); + curl_free(esc); + } + } + } + } + + // if necessary add a path separator + if(p.ptr[p.length-1] == '/') { + if(href.space[href.pos-1] != '/') { + cxBufferPut(&href, '/'); + } + cxBufferPut(&pbuf, '/'); + } + // add the final path to the cache + cxstring pp = cx_strn(pbuf.space, pbuf.size); + cxstring hh = cx_strn(href.space, href.size); + dav_session_cache_path(sn, pp, hh); + + cxmutstr href_str = cx_strdup_a( + sn->mp->allocator, + cx_strn(href.space, href.size)); + + // cleanup + dav_resource_free_all(root); + cxBufferFree(rqbuf); + + cxBufferDestroy(&pbuf); + cxBufferDestroy(&href); + + return href_str.ptr; + } else { + return dav_session_create_plain_href(sn, path); + } +} + +DavResource* dav_find_child(DavSession *sn, DavResource *res, CxBuffer *rqbuf, const char *name) { + if(res && !dav_propfind(sn, res, rqbuf)) { + DavResource *child = res->children; + while(child) { + if(!strcmp(child->name, name)) { + return child; + } + child = child->next; + } + } + return NULL; +} + +void dav_session_cache_path(DavSession *sn, cxstring path, cxstring href) { + CxHashKey path_key = cx_hash_key(path.ptr, path.length); + char *elm = cxMapGet(sn->pathcache, path_key); + if(!elm) { + cxmutstr href_s = cx_strdup_a(sn->mp->allocator, href); + cxMapPut(sn->pathcache, path_key, href_s.ptr); + } +} + + +DavLock* dav_create_lock(DavSession *sn, const char *token, char *timeout) { + DavLock *lock = dav_session_malloc(sn, sizeof(DavLock)); + lock->path = NULL; + lock->token = dav_session_strdup(sn, token); + + // TODO: timeout + + return lock; +} + +void dav_destroy_lock(DavSession *sn, DavLock *lock) { + dav_session_free(sn, lock->token); + if(lock->path) { + dav_session_free(sn, lock->path); + } + dav_session_free(sn, lock); +} + + +static int dav_lock_cmp(void const *left, void const *right) { + const DavLock *l = left; + const DavLock *r = right; + return strcmp(l->path, r->path); +} + +static int create_lock_manager(DavSession *sn) { + // create lock manager + DavLockManager *locks = cxMalloc(sn->mp->allocator, sizeof(DavLockManager)); + locks->resource_locks = cxHashMapCreate(sn->mp->allocator, CX_STORE_POINTERS, 16); + locks->collection_locks = cxLinkedListCreate(sn->mp->allocator, dav_lock_cmp, CX_STORE_POINTERS); + sn->locks = locks; + return 0; +} + +static DavLockManager* get_lock_manager(DavSession *sn) { + DavLockManager *locks = sn->locks; + if(!locks) { + if(create_lock_manager(sn)) { + return NULL; + } + locks = sn->locks; + } + return locks; +} + +int dav_add_resource_lock(DavSession *sn, const char *path, DavLock *lock) { + DavLockManager *locks = get_lock_manager(sn); + if(!locks) { + return -1; + } + + CxHashKey path_key = cx_hash_key_str(path); + if(cxMapGet(locks->resource_locks, path_key)) { + return -1; + } + + cxMapPut(locks->resource_locks, path_key, lock); + return 0; +} + +int dav_add_collection_lock(DavSession *sn, const char *path, DavLock *lock) { + DavLockManager *locks = get_lock_manager(sn); + if(!locks) { + return -1; + } + + lock->path = dav_session_strdup(sn, path); + cxListAdd(locks->collection_locks, lock); + cxListSort(locks->collection_locks); + + return 0; +} + +DavLock* dav_get_lock(DavSession *sn, const char *path) { + DavLockManager *locks = get_lock_manager(sn); + if(!locks) { + return NULL; + } + + cxstring p = cx_str(path); + + DavLock *lock = cxMapGet(locks->resource_locks, cx_hash_key(p.ptr, p.length)); + if(lock) { + return lock; + } + + CxIterator i = cxListIterator(locks->collection_locks); + cx_foreach(DavLock*, col_lock, i) { + int cmd = strcmp(path, col_lock->path); + if(cmd == 0) { + return col_lock; + } else if(cx_strprefix(p, cx_str(col_lock->path))) { + return col_lock; + } else if(cmd > 0) { + break; + } + } + + return NULL; +} + +void dav_remove_lock(DavSession *sn, const char *path, DavLock *lock) { + DavLockManager *locks = get_lock_manager(sn); + if(!locks) { + return; + } + + if(cxMapRemoveAndGet(locks->resource_locks, cx_hash_key_str(path))) { + return; + } + + CxMutIterator i = cxListMutIterator(locks->collection_locks); + int rm = 0; + cx_foreach(DavLock* , cl, i) { + if(rm) { + break; + } + if(cl == lock) { + cxIteratorFlagRemoval(i); + rm = 1; + } + } +}