caldav.c (21520B)
1 #include "caldav.h" 2 3 CURL* curl; 4 char access_token[sizeof(char)*2048] = ""; 5 6 // Local bind address for receiving OAuth callbacks. 7 // Reserve 2 chars for the ipv6 square brackets. 8 char ip[INET6_ADDRSTRLEN], ipstr[INET6_ADDRSTRLEN + 2]; 9 10 // Static XML data for CalDav post requests: 11 // https://write.in0rdr.ch/caldav-calendar-discovery 12 const char* principal_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/'>" 13 "<d:prop><d:current-user-principal/></d:prop>" 14 "</d:propfind>"; 15 const char* homeset_request = "<d:propfind xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" 16 " <d:prop>" 17 " <c:calendar-home-set />" 18 " </d:prop>" 19 "</d:propfind>"; 20 const char* calendar_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:c='urn:ietf:params:xml:ns:caldav'>" 21 " <d:prop>" 22 " <d:resourcetype />" 23 " <d:displayname />" 24 " <cs:getctag />" 25 " <c:supported-calendar-component-set />" 26 " </d:prop>" 27 "</d:propfind>"; 28 char* events_request = "<c:calendar-query xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" 29 "<d:prop><d:getetag/><c:calendar-data/></d:prop>" 30 "<c:filter><c:comp-filter name='VCALENDAR'>" 31 "<c:comp-filter name='VEVENT'>" 32 "<c:time-range start='%s' end='%s'/></c:comp-filter>" 33 "</c:comp-filter></c:filter></c:calendar-query>"; 34 35 static size_t curl_write_mem_callback(void* contents, size_t size, size_t nmemb, void* userp) { 36 size_t realsize = size * nmemb; 37 struct curl_mem_chunk* mem = (struct curl_mem_chunk*)userp; 38 39 char* ptr = realloc(mem->memory, mem->size + realsize + 1); 40 if (!ptr) { 41 tracepoint(diary, error, "Not enough memory (realloc in CURLOPT_WRITEFUNCTION returned NULL)"); 42 return 0; 43 } 44 45 mem->memory = ptr; 46 memcpy(&(mem->memory[mem->size]), contents, realsize); 47 mem->size += realsize; 48 mem->memory[mem->size] = 0; 49 50 return realsize; 51 } 52 53 // todo 54 // https://beej.us/guide/bgnet/html 55 void* get_in_addr(struct sockaddr* sa) { 56 if (sa->sa_family == AF_INET) { 57 return &(((struct sockaddr_in*)sa)->sin_addr); 58 } 59 60 return &(((struct sockaddr_in6*)sa)->sin6_addr); 61 } 62 63 /* Get access token from "oauth_eval_cmd" and refresh global var */ 64 void get_access_token() { 65 FILE *fp; 66 int status; 67 68 fp = popen(CONFIG.oauth_eval_cmd, "r"); 69 if (fp == NULL) 70 perror("Failure running oauth_eval_cmd in get_access_token()"); 71 72 while (fgets(access_token, sizeof(char)*2048, fp) != NULL) 73 tracepoint(diary, debug_string, "New access token in get_access_token()", access_token); 74 75 // strip any newline characters 76 access_token[strcspn(access_token, "\n")] = '\0'; 77 78 status = pclose(fp); 79 if (status == -1) { 80 perror("Failure during pclose in get_access_token()"); 81 } 82 } 83 84 /* Make a CalDAV request */ 85 char* caldav_req(struct tm* date, char* url, char* http_method, const char* postfields, int depth, bool basicauth, char* content_type, long num_redirects) { 86 // only support depths 0 or 1 87 if (depth < 0 || depth > 1) { 88 return NULL; 89 } 90 91 CURLcode res; 92 93 curl = curl_easy_init(); 94 95 // https://curl.se/libcurl/c/getinmemory.html 96 struct curl_mem_chunk caldav_resp; 97 caldav_resp.memory = malloc(1); 98 if (caldav_resp.memory == NULL) { 99 perror("malloc failed"); 100 return NULL; 101 } 102 caldav_resp.size = 0; 103 104 if (curl) { 105 // fail if not authenticated, !CURLE_OK 106 curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); 107 //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); 108 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); 109 tracepoint(diary, debug_string, "curl_easy_perform() request url", url); 110 curl_easy_setopt(curl, CURLOPT_URL, url); 111 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_mem_callback); 112 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&caldav_resp); 113 114 // construct header fields 115 struct curl_slist *header = NULL; 116 117 // prefer basicauth credentials when set 118 if (basicauth) { 119 char basicauth [strlen(CONFIG.caldav_username) + strlen(CONFIG.caldav_password) + 2]; 120 sprintf(basicauth, "%s:%s", CONFIG.caldav_username, CONFIG.caldav_password); 121 curl_easy_setopt(curl, CURLOPT_USERPWD, basicauth); 122 } else { 123 // use OAuth credentials 124 char bearer_token[strlen("Authorization: Bearer ") + strlen(access_token) + 1]; 125 sprintf(bearer_token, "Authorization: Bearer %s", access_token); 126 header = curl_slist_append(header, bearer_token); 127 } 128 129 char depth_header[strlen("Depth: 0") + 1]; 130 sprintf(depth_header, "Depth: %i", depth); 131 header = curl_slist_append(header, depth_header); 132 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header); 133 134 if (strcmp(content_type, "") != 0 && content_type != NULL) { 135 char contentType[strlen("Content-Type: ") + strlen(content_type) + 1]; 136 sprintf(contentType, "Content-Type: %s", content_type); 137 header = curl_slist_append(header, contentType); 138 } 139 140 // set postfields, if any 141 if (postfields != NULL) { 142 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields); 143 } 144 145 // enable redirect following, some providers have certain caldav 146 // endpoinds on sub-paths 147 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 148 curl_easy_setopt(curl, CURLOPT_MAXREDIRS, num_redirects); 149 150 res = curl_easy_perform(curl); 151 152 long response_code = 0; 153 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); 154 155 // free the memory used for the curl slist 156 curl_slist_free_all(header); 157 158 curl_easy_cleanup(curl); 159 160 if (res != CURLE_OK) { 161 tracepoint(diary, error_string, "curl_easy_perform() failed in caldav_req()", curl_easy_strerror(res)); 162 tracepoint(diary, debug_long, "curl_easy_perform() response code", response_code); 163 free(caldav_resp.memory); 164 return NULL; 165 } 166 } 167 168 return caldav_resp.memory; 169 } 170 171 /* Upload event to CalDAV server */ 172 void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar_uri, bool basicauth) { 173 // get entry path 174 char path[100]; 175 char* ppath = path; 176 char* descr; 177 long descr_bytes; 178 179 fpath(dir, dir_size, date, &ppath, sizeof path); 180 if (ppath == NULL) { 181 tracepoint(diary, error_date, "Cannot get file path for entry", date); 182 return; 183 } 184 185 FILE* fp = fopen(path, "r"); 186 if (fp == NULL) perror("Error opening file"); 187 188 fseek(fp, 0, SEEK_END); 189 descr_bytes = ftell(fp); 190 rewind(fp); 191 192 size_t descr_label_size = strlen("DESCRIPTION:"); 193 size_t descr_size = descr_bytes + descr_label_size; 194 descr = calloc(descr_size + 1, sizeof(char)); 195 if (descr == NULL) { 196 perror("malloc failed"); 197 fclose(fp); 198 return; 199 } 200 201 strcat(descr, "DESCRIPTION:"); 202 203 size_t items_read = fread(descr + descr_label_size, sizeof(char), descr_bytes, fp); 204 if (items_read != descr_bytes) { 205 perror("Error while reading in put_event()"); 206 fclose(fp); 207 return; 208 } 209 210 descr[descr_size] = '\0'; 211 char* folded_descr = fold(descr); 212 213 char uid[9]; 214 strftime(uid, sizeof uid, "%Y%m%d", date); 215 216 char* ics = "BEGIN:VCALENDAR\n" 217 "PRODID:-//diary\n" // todo: add version 218 "VERSION:2.0\n" 219 "CALSCALE:GREGORIAN\n" 220 "BEGIN:VEVENT\n" 221 "UID:%s\n" 222 "DTSTART;VALUE=DATE:%s\n" 223 "SUMMARY:%s\n" 224 "%s\n" 225 "END:VEVENT\n" 226 "END:VCALENDAR"; 227 char postfields[strlen(ics) + strlen(folded_descr) + 100]; 228 sprintf(postfields, ics, 229 uid, 230 uid, 231 uid, // todo: display first few chars of DESCRIPTION as SUMMARY 232 folded_descr); 233 234 tracepoint(diary, debug_string, "PUT text/calendar", postfields); 235 236 strcat(calendar_uri, uid); 237 strcat(calendar_uri, ".ics"); 238 char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0, basicauth, "text/calendar", 0); 239 fclose(fp); 240 free(folded_descr); 241 free(descr); 242 243 if (response == NULL) { 244 tracepoint(diary, error, "PUT request for event failed in put_event()"); 245 } 246 247 // free memory allocated to store curl response 248 free(response); 249 } 250 251 /* 252 * Sync with CalDAV server. 253 * Returns the answer char of the confirmation dialogue 254 * Returns 0 if neither local nor remote file exists. 255 * Otherwise, returns -1 on error. 256 */ 257 int caldav_sync(struct tm* date, 258 WINDOW* header, 259 WINDOW* cal, 260 int pad_pos, 261 const char* dir, 262 size_t dir_size, 263 bool confirm) { 264 pthread_t progress_tid; 265 266 bool oauth_enabled = !(strcmp(CONFIG.oauth_eval_cmd, "") == 0); 267 bool basicauth_enabled = !(strcmp(CONFIG.caldav_username, "") == 0 || strcmp(CONFIG.caldav_password, "") == 0); 268 tracepoint(diary, debug_int, "oauth_enabled: ", oauth_enabled); 269 tracepoint(diary, debug_int, "basicauth_enabled", basicauth_enabled); 270 271 // check remote server and calendar are defined 272 if (strcmp(CONFIG.caldav_server, "") == 0 || strcmp(CONFIG.caldav_calendar, "") == 0) { 273 tracepoint(diary, debug_string, "CONFIG.caldav_server", CONFIG.caldav_server); 274 tracepoint(diary, debug_string, "CONFIG.caldav_calendar", CONFIG.caldav_calendar); 275 tracepoint(diary, debug_string, "CONFIG.caldav_username", CONFIG.caldav_username); 276 tracepoint(diary, debug_string, "CONFIG.caldav_password", CONFIG.caldav_password); 277 show_info(header, "CalDAV config incomplete, press any key to continue.", NULL); 278 return -1; 279 } 280 281 if (oauth_enabled) { 282 if (strcmp(access_token, "") == 0) { 283 // get acess token with oauth_eval_cmd 284 tracepoint(diary, debug_string, "Fetching access token using oauth_eval_cmd", CONFIG.oauth_eval_cmd); 285 get_access_token(); 286 tracepoint(diary, debug_string, "OAuth access token", access_token); 287 } 288 } 289 290 pthread_create(&progress_tid, NULL, show_progress, (void*)header); 291 pthread_detach(progress_tid); 292 293 char* user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_request, 0, basicauth_enabled, "application/xml", 1); 294 tracepoint(diary, debug_string, "User principal return XML", user_principal); 295 296 if (user_principal == NULL) { 297 // The principal could not be fetched 298 tracepoint(diary, debug, "Unable to fetch user principal"); 299 show_info(header, "Unable to fetch user principal, press any key to continue.", &progress_tid); 300 return -1; 301 } 302 303 char* current_user_principal = extract_xml_content( 304 user_principal, 305 "//*[local-name()='current-user-principal']/*[local-name()='href']", 306 header, 307 &progress_tid); 308 309 // free memory allocated by curl request 310 free(user_principal); 311 312 if (current_user_principal == NULL) { 313 tracepoint(diary, debug, "Unable to parse current user principal"); 314 show_info(header, "Unable to parse current user principal, press any key to continue.", &progress_tid); 315 return -1; 316 } 317 318 // extract host without path as basis for further api paths 319 char *caldav_host; char* caldav_host_scheme; 320 CURLU *h = curl_url(); CURLUcode uc = curl_url_set(h, CURLUPART_URL, CONFIG.caldav_server, 0); uc = curl_url_get(h, CURLUPART_HOST, &caldav_host, 0); 321 uc = curl_url_get(h, CURLUPART_SCHEME, &caldav_host_scheme, 0); 322 tracepoint(diary, debug_string, "Caldav server host name", caldav_host); 323 tracepoint(diary, debug_string, "Caldav server scheme/protocol", caldav_host_scheme); 324 tracepoint(diary, debug_string, "Caldav server host name", caldav_host); 325 if (!uc) { 326 tracepoint(diary, error, "cur_url_get() failed in caldav_sync()"); 327 } 328 curl_url_cleanup(h); 329 330 // get the home-set of the user 331 char uri[300]; 332 sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, current_user_principal); 333 334 char* home_set = caldav_req(date, uri, "PROPFIND", homeset_request, 0, basicauth_enabled, "application/xml", 0); 335 tracepoint(diary, debug_string, "Home set xml", home_set); 336 337 // parse home set from xml response 338 char* home_set_parsed = extract_xml_content( 339 home_set, 340 "//*[local-name()='calendar-home-set']/*[local-name()='href']", 341 header, 342 &progress_tid); 343 344 if (home_set_parsed == NULL) { 345 tracepoint(diary, debug, "Unable to parse home set"); 346 show_info(header, "Unable to parse home set, press any key to continue.", &progress_tid); 347 return -1; 348 } 349 350 sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, home_set_parsed); 351 352 free(home_set); 353 free(home_set_parsed); 354 355 // get calendar from home-set 356 char* calendar_xml = caldav_req(date, uri, "PROPFIND", calendar_request, 1, basicauth_enabled, "application/xml", 0); 357 tracepoint(diary, debug_string, "Calendar set xml", calendar_xml); 358 359 // get calendar URI from the home-set 360 char calendar_xpath_query[300]; 361 sprintf(calendar_xpath_query, "//*[local-name()='displayname'][text()='%s']/../../../*[local-name()='href']", CONFIG.caldav_calendar); 362 tracepoint(diary, debug_string, "Calendar xpath query", calendar_xpath_query); 363 char* calendar_href = extract_xml_content( 364 calendar_xml, 365 calendar_xpath_query, 366 header, 367 &progress_tid); 368 369 if (calendar_href == NULL) { 370 tracepoint(diary, debug_string, "Could not find CalDAV calendar", CONFIG.caldav_calendar); 371 char* msg_fmtstr = "Could not find CalDAV calendar '%s', press any key to continue."; 372 char msg[strlen(msg_fmtstr) + strlen(CONFIG.caldav_calendar)]; 373 sprintf(msg, msg_fmtstr, CONFIG.caldav_calendar); 374 show_info(header, msg, &progress_tid); 375 return -1; 376 } 377 378 free(calendar_xml); 379 380 char dstr_start[30]; 381 char dstr_end[30]; 382 char* format_start = "%Y%m%dT000000Z"; // start of day 383 char* format_end = "%Y%m%dT235900Z"; // end of day 384 strftime(dstr_start, sizeof dstr_start, format_start, date); 385 strftime(dstr_end, sizeof dstr_end, format_end, date); 386 387 char caldata_postfields[strlen(events_request) + strlen(dstr_start) + strlen(dstr_end)]; 388 sprintf(caldata_postfields, events_request, 389 dstr_start, 390 dstr_end); 391 392 // fetch event for the cursor date 393 sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, calendar_href); 394 395 char* events_xml = caldav_req(date, uri, "REPORT", caldata_postfields, 1, basicauth_enabled, "application/xml", 0); 396 397 if (events_xml == NULL) { 398 tracepoint(diary, debug, "Events could not be fetched"); 399 show_info(header, "Events could not be fetched", &progress_tid); 400 return -1; 401 } 402 403 // only fetch PRODID diary events 404 char* event = extract_xml_content( 405 events_xml, 406 "//*[local-name()='calendar-data' and contains(text(), 'PRODID:-//diary')]", 407 header, 408 &progress_tid); 409 410 free(events_xml); 411 412 // no remote event found 413 if (event == NULL) { 414 tracepoint(diary, debug, "No remote events found, continuing."); 415 event = ""; 416 } 417 418 // get path of entry 419 char path[100]; 420 char* ppath = path; 421 fpath(CONFIG.dir, strlen(CONFIG.dir), date, &ppath, sizeof path); 422 423 time_t epoch_zero = 0; 424 425 // assume local file does not exist 426 struct tm* localfile_time = gmtime(&epoch_zero); 427 428 // check last modification time of local time 429 struct stat attr; 430 if (stat(path, &attr) == 0) { 431 // if local file does exists, remember utc time of file on disk 432 localfile_time = gmtime(&attr.st_mtime); 433 } 434 435 time_t localfile_date = mktime(localfile_time); 436 437 struct tm* remote_datetime; 438 time_t remote_date = 0; 439 long search_pos = 0; 440 441 // check remote DTSTAMP of remote event 442 char* remote_dtstamp = extract_ical_field(event, "DTSTAMP", &search_pos, false); 443 char* remote_uid = extract_ical_field(event, "UID", &search_pos, false); 444 tracepoint(diary, debug_string, "event", event); 445 tracepoint(diary, debug_string, "DTSTAMP field", remote_dtstamp); 446 447 // init remote date to 1970, assume remote file does not exist 448 remote_datetime = gmtime(&epoch_zero); 449 450 if (remote_dtstamp != NULL) { 451 // if remote date does exist, set date to the correct timestamp 452 strptime(remote_dtstamp, "%Y%m%dT%H%M%SZ", remote_datetime); 453 free(remote_dtstamp); 454 } 455 456 remote_date = mktime(remote_datetime); 457 458 tracepoint(diary, debug_string, "Remote last modified", ctime(&remote_date)); 459 tracepoint(diary, debug_string, "Local last modified", ctime(&localfile_date)); 460 double timediff = difftime(localfile_date, remote_date); 461 tracepoint(diary, debug_double, "Time diff between local and remote mod time", timediff); 462 463 char* rmt_desc; 464 char dstr[16]; 465 int conf_ch = 0; 466 467 if (timediff == 0) { 468 tracepoint(diary, debug, "Local and remote files have equal timestamp or don't exist, giving up."); 469 pthread_cancel(progress_tid); 470 wclear(header); 471 // free memory allocated to store curl response 472 curl_free(caldav_host_scheme); 473 curl_free(caldav_host); 474 free(remote_uid); 475 return 0; 476 } else if (timediff > 0) { 477 tracepoint(diary, debug, "Local file is newer. Uploading to remote."); 478 if (remote_uid) { 479 // purge any existing daily calendar entries on the remote side 480 char event_uri[300]; 481 sprintf(event_uri, "%s://%s%s%s.ics", caldav_host_scheme, caldav_host, calendar_href, remote_uid); 482 //sprintf(event_uri, "%s%s%s.ics", CONFIG.caldav_server, calendar_href, remote_uid); 483 tracepoint(diary, debug_string, "Event URI for DELETE request", event_uri); 484 char* response = caldav_req(date, event_uri, "DELETE", NULL, 0, basicauth_enabled, "", 0); 485 free(response); 486 } 487 488 put_event(date, dir, dir_size, uri, basicauth_enabled); 489 490 pthread_cancel(progress_tid); 491 wclear(header); 492 493 } else if (timediff < 0) { 494 rmt_desc = extract_ical_field(event, "DESCRIPTION", &search_pos, true); 495 496 if (rmt_desc == NULL) { 497 tracepoint(diary, error, "Could not fetch description of remote event. Aborting sync."); 498 pthread_cancel(progress_tid); 499 wclear(header); 500 curl_free(caldav_host_scheme); 501 curl_free(caldav_host); 502 free(remote_uid); 503 return -1; 504 } 505 506 if (confirm) { 507 // prepare header for confirmation dialogue 508 curs_set(2); 509 noecho(); 510 pthread_cancel(progress_tid); 511 wclear(header); 512 513 // ask for confirmation 514 strftime(dstr, sizeof dstr, CONFIG.fmt, date); 515 mvwprintw(header, 0, 0, "Remote event is more recent. Sync entry '%s' and overwrite local file? [(Y)es/(a)ll/(n)o/(c)ancel] ", dstr); 516 conf_ch = wgetch(header); 517 } 518 519 if (conf_ch == 'y' || conf_ch == 'Y' || conf_ch == 'a' || conf_ch == '\n' || !confirm) { 520 tracepoint(diary, debug, "Remote file is newer. Extracting description from remote."); 521 char* i; 522 523 // persist downloaded buffer to local file 524 FILE* cursordate_file = fopen(path, "wb"); 525 if (cursordate_file == NULL) { 526 perror("Failed to open cursor date file"); 527 } else { 528 for (i = rmt_desc; *i != '\0'; i++) { 529 if (rmt_desc[i-rmt_desc] == 0x5C) { // backslash 530 switch (*(i+1)) { 531 case 'n': 532 fputc('\n', cursordate_file); 533 i++; 534 break; 535 case 0x5c: // preserve real backslash 536 fputc(0x5c, cursordate_file); 537 i++; 538 break; 539 } 540 } else { 541 fputc(*i, cursordate_file); 542 } 543 } 544 } 545 fclose(cursordate_file); 546 547 // add new entry highlight 548 chtype atrs = winch(cal) & A_ATTRIBUTES; 549 wchgat(cal, 2, atrs | A_BOLD, 0, NULL); 550 prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH); 551 } 552 553 echo(); 554 curs_set(0); 555 free(rmt_desc); 556 } 557 558 // free memory allocated to store curl response 559 curl_free(caldav_host_scheme); 560 curl_free(caldav_host); 561 free(remote_uid); 562 free(calendar_href); 563 pthread_join(progress_tid, NULL); 564 wclear(header); 565 return conf_ch; 566 }