caldav.c (21545B)
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 debug("%s", "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 debug("New access token in get_access_token(): %s", 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 debug("curl_easy_perform() request url: %s", 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 debug("curl_easy_perform() failed in caldav_req(): %s", curl_easy_strerror(res)); 162 debug("curl_easy_perform() response code: %li", 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 debug("Cannot get file path for entry %s", asctime(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 debug("PUT text/calendar %s", 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 debug("%s", "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 debug("oauth_enabled: %i", oauth_enabled); 269 debug("basicauth_enabled: %i", basicauth_enabled); 270 271 // check remote server and calendar are defined 272 if (strcmp(CONFIG.caldav_server, "") == 0 || strcmp(CONFIG.caldav_calendar, "") == 0) { 273 debug("CONFIG.caldav_server: %s", CONFIG.caldav_server); 274 debug("CONFIG.caldav_calendar: %s", CONFIG.caldav_calendar); 275 debug("CONFIG.caldav_username: %s", CONFIG.caldav_username); 276 debug("CONFIG.caldav_password: %s", 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 debug("Fetching access token using oauth_eval_cmd %s", CONFIG.oauth_eval_cmd); 285 get_access_token(); 286 debug("OAuth access token: %s", 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 debug("User principal return XML %s", user_principal); 295 296 if (user_principal == NULL) { 297 // The principal could not be fetched 298 debug("%s", "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 debug("%s", "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_port; 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 uc = curl_url_get(h, CURLUPART_PORT, &caldav_port, 0); 323 debug("Caldav server host name: %s", caldav_host); 324 debug("Caldav server port: %s", caldav_port); 325 debug("Caldav server scheme/protocol: %s", caldav_host_scheme); 326 if (!uc) { 327 debug("%s", "cur_url_get() failed in caldav_sync()"); 328 } 329 curl_url_cleanup(h); 330 331 // get the home-set of the user 332 char uri[300]; 333 if (caldav_port == NULL) { 334 sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, current_user_principal); 335 } else { 336 sprintf(uri, "%s://%s:%s%s", caldav_host_scheme, caldav_host, caldav_port, current_user_principal); 337 } 338 339 char* home_set = caldav_req(date, uri, "PROPFIND", homeset_request, 0, basicauth_enabled, "application/xml", 0); 340 debug("Home set xml %s", home_set); 341 342 // parse home set from xml response 343 char* home_set_parsed = extract_xml_content( 344 home_set, 345 "//*[local-name()='calendar-home-set']/*[local-name()='href']", 346 header, 347 &progress_tid); 348 349 if (home_set_parsed == NULL) { 350 debug("%s", "Unable to parse home set"); 351 show_info(header, "Unable to parse home set, press any key to continue.", &progress_tid); 352 return -1; 353 } 354 355 if (caldav_port == NULL) { 356 sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, home_set_parsed); 357 } else { 358 sprintf(uri, "%s://%s:%s%s", caldav_host_scheme, caldav_host, caldav_port, home_set_parsed); 359 } 360 361 free(home_set); 362 free(home_set_parsed); 363 364 // get calendar from home-set 365 char* calendar_xml = caldav_req(date, uri, "PROPFIND", calendar_request, 1, basicauth_enabled, "application/xml", 0); 366 debug("Calendar set xml: %s", calendar_xml); 367 368 // get calendar URI from the home-set 369 char calendar_xpath_query[300]; 370 sprintf(calendar_xpath_query, "//*[local-name()='displayname'][text()='%s']/../../../*[local-name()='href']", CONFIG.caldav_calendar); 371 debug("Calendar xpath query: %s", calendar_xpath_query); 372 char* calendar_href = extract_xml_content( 373 calendar_xml, 374 calendar_xpath_query, 375 header, 376 &progress_tid); 377 378 if (calendar_href == NULL) { 379 debug("Could not find CalDAV calendar %s", CONFIG.caldav_calendar); 380 char* msg_fmtstr = "Could not find CalDAV calendar '%s', press any key to continue."; 381 char msg[strlen(msg_fmtstr) + strlen(CONFIG.caldav_calendar)]; 382 sprintf(msg, msg_fmtstr, CONFIG.caldav_calendar); 383 show_info(header, msg, &progress_tid); 384 return -1; 385 } 386 387 free(calendar_xml); 388 389 char dstr_start[30]; 390 char dstr_end[30]; 391 char* format_start = "%Y%m%dT000000Z"; // start of day 392 char* format_end = "%Y%m%dT235900Z"; // end of day 393 strftime(dstr_start, sizeof dstr_start, format_start, date); 394 strftime(dstr_end, sizeof dstr_end, format_end, date); 395 396 char caldata_postfields[strlen(events_request) + strlen(dstr_start) + strlen(dstr_end)]; 397 sprintf(caldata_postfields, events_request, 398 dstr_start, 399 dstr_end); 400 401 // fetch event for the cursor date 402 if (caldav_port == NULL) { 403 sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, calendar_href); 404 } else { 405 sprintf(uri, "%s://%s:%s%s", caldav_host_scheme, caldav_host, caldav_port, calendar_href); 406 } 407 408 char* events_xml = caldav_req(date, uri, "REPORT", caldata_postfields, 1, basicauth_enabled, "application/xml", 0); 409 410 if (events_xml == NULL) { 411 debug("%s", "Events could not be fetched"); 412 show_info(header, "Events could not be fetched", &progress_tid); 413 return -1; 414 } 415 416 // only fetch PRODID diary events 417 char* event = extract_xml_content( 418 events_xml, 419 "//*[local-name()='calendar-data' and contains(text(), 'PRODID:-//diary')]", 420 header, 421 &progress_tid); 422 423 free(events_xml); 424 425 // no remote event found 426 if (event == NULL) { 427 debug("%s", "No remote events found, continuing."); 428 event = ""; 429 } 430 431 // get path of entry 432 char path[100]; 433 char* ppath = path; 434 fpath(CONFIG.dir, strlen(CONFIG.dir), date, &ppath, sizeof path); 435 436 time_t epoch_zero = 0; 437 438 // assume local file does not exist 439 struct tm* localfile_time = gmtime(&epoch_zero); 440 441 // check last modification time of local time 442 struct stat attr; 443 if (stat(path, &attr) == 0) { 444 // if local file does exists, remember utc time of file on disk 445 localfile_time = gmtime(&attr.st_mtime); 446 } 447 448 time_t localfile_date = mktime(localfile_time); 449 450 struct tm* remote_datetime; 451 time_t remote_date = 0; 452 long search_pos = 0; 453 454 // check remote DTSTAMP of remote event 455 char* remote_dtstamp = extract_ical_field(event, "DTSTAMP", &search_pos, false); 456 char* remote_uid = extract_ical_field(event, "UID", &search_pos, false); 457 debug("Event: %s", event); 458 debug("DTSTAMP field: %s", remote_dtstamp); 459 460 // init remote date to 1970, assume remote file does not exist 461 remote_datetime = gmtime(&epoch_zero); 462 463 if (remote_dtstamp != NULL) { 464 // if remote date does exist, set date to the correct timestamp 465 strptime(remote_dtstamp, "%Y%m%dT%H%M%SZ", remote_datetime); 466 free(remote_dtstamp); 467 } 468 469 remote_date = mktime(remote_datetime); 470 471 debug("Remote last modified: %s", ctime(&remote_date)); 472 debug("Local last modified: %s", ctime(&localfile_date)); 473 double timediff = difftime(localfile_date, remote_date); 474 debug("Time diff between local and remote mod time: %f", timediff); 475 476 char* rmt_desc; 477 char dstr[16]; 478 int conf_ch = 0; 479 480 if (timediff == 0) { 481 debug("%s", "Local and remote files have equal timestamp or don't exist, giving up."); 482 pthread_cancel(progress_tid); 483 wclear(header); 484 // free memory allocated to store curl response 485 curl_free(caldav_host_scheme); 486 curl_free(caldav_host); 487 curl_free(caldav_port); 488 free(remote_uid); 489 return 0; 490 } else if (timediff > 0) { 491 debug("%s", "Local file is newer. Uploading to remote."); 492 if (remote_uid) { 493 // purge any existing daily calendar entries on the remote side 494 char event_uri[300]; 495 if (caldav_port == NULL) { 496 sprintf(event_uri, "%s://%s:%s%s.ics", caldav_host_scheme, caldav_host, calendar_href, remote_uid); 497 } else { 498 sprintf(event_uri, "%s://%s:%s%s%s.ics", caldav_host_scheme, caldav_host, caldav_port, calendar_href, remote_uid); 499 } 500 //sprintf(event_uri, "%s%s%s.ics", CONFIG.caldav_server, calendar_href, remote_uid); 501 debug("Event URI for DELETE request: %s", event_uri); 502 char* response = caldav_req(date, event_uri, "DELETE", NULL, 0, basicauth_enabled, "", 0); 503 free(response); 504 } 505 506 put_event(date, dir, dir_size, uri, basicauth_enabled); 507 508 pthread_cancel(progress_tid); 509 wclear(header); 510 511 } else if (timediff < 0) { 512 rmt_desc = extract_ical_field(event, "DESCRIPTION", &search_pos, true); 513 514 if (rmt_desc == NULL) { 515 debug("%s", "Could not fetch description of remote event. Aborting sync."); 516 pthread_cancel(progress_tid); 517 wclear(header); 518 curl_free(caldav_host_scheme); 519 curl_free(caldav_host); 520 free(remote_uid); 521 return -1; 522 } 523 524 if (confirm) { 525 // prepare header for confirmation dialogue 526 curs_set(2); 527 noecho(); 528 pthread_cancel(progress_tid); 529 wclear(header); 530 531 // ask for confirmation 532 strftime(dstr, sizeof dstr, CONFIG.fmt, date); 533 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); 534 conf_ch = wgetch(header); 535 } 536 537 if (conf_ch == 'y' || conf_ch == 'Y' || conf_ch == 'a' || conf_ch == '\n' || !confirm) { 538 debug("%s", "Remote file is newer. Extracting description from remote."); 539 char* i; 540 541 // persist downloaded buffer to local file 542 FILE* cursordate_file = fopen(path, "wb"); 543 if (cursordate_file == NULL) { 544 perror("Failed to open cursor date file"); 545 } else { 546 for (i = rmt_desc; *i != '\0'; i++) { 547 if (rmt_desc[i-rmt_desc] == 0x5C) { // backslash 548 switch (*(i+1)) { 549 case 'n': 550 fputc('\n', cursordate_file); 551 i++; 552 break; 553 case 0x5c: // preserve real backslash 554 fputc(0x5c, cursordate_file); 555 i++; 556 break; 557 } 558 } else { 559 fputc(*i, cursordate_file); 560 } 561 } 562 } 563 fclose(cursordate_file); 564 565 // add new entry highlight 566 chtype atrs = winch(cal) & A_ATTRIBUTES; 567 wchgat(cal, 2, atrs | A_BOLD, 0, NULL); 568 prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH); 569 } 570 571 echo(); 572 curs_set(0); 573 free(rmt_desc); 574 } 575 576 // free memory allocated to store curl response 577 curl_free(caldav_host_scheme); 578 curl_free(caldav_host); 579 free(remote_uid); 580 free(calendar_href); 581 pthread_join(progress_tid, NULL); 582 wclear(header); 583 return conf_ch; 584 }