diary

Text-based journaling program
git clone https://git.in0rdr.ch/diary.git
Log | Files | Refs | README | LICENSE

commit 6cf6c6647f700c016a6d97458934f39b182afd32
parent 473b0067aa5f1c593a43e795e0cd512e3d3d9a9c
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Mon, 17 Jun 2024 21:45:04 +0200

feat(caldav): caldav sync with yahoo

Fixes:
* fix several printf buffer overflows
* fix xml parsing functions to not assume xml namespace, can be different
  depending on the provider
* fix wrong caldav report depth header fields
* fix wrong caldav request time-range xml filter that synced down the event
  from the next day to the currently selected day
* fix missing CR while unfolding

Adds:
* send proper content-type
* new debug_long tracepoint
* improve homeset and calendar requests
* replace LAST-MODIFIED with DTSTAMP, because LAST-MODIFIED is not available on
  all providers

Diffstat:
Msrc/caldav.c | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/diary-tp.h | 14++++++++++++++
Msrc/utils.c | 4++--
3 files changed, 123 insertions(+), 54 deletions(-)

diff --git a/src/caldav.c b/src/caldav.c @@ -407,7 +407,7 @@ char* get_oauth_code(const char* verifier, WINDOW* header) { } /* Make a CalDAV request */ -char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields, int depth, bool basicauth) { +char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields, int depth, bool basicauth, char* content_type) { // only support depths 0 or 1 if (depth < 0 || depth > 1) { return NULL; @@ -431,27 +431,37 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); + tracepoint(diary, debug_string, "curl_easy_perform() request url", url); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_mem_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&caldav_resp); - // default to basic auth when Google credentials are not set + // construct header fields + struct curl_slist *header = NULL; + + // default to basic auth when Google credentials are not set if (basicauth) { - char basicauth [strlen(CONFIG.caldav_username) + strlen(CONFIG.caldav_password) + 1]; + char basicauth [strlen(CONFIG.caldav_username) + strlen(CONFIG.caldav_password) + 2]; sprintf(basicauth, "%s:%s", CONFIG.caldav_username, CONFIG.caldav_password); curl_easy_setopt(curl, CURLOPT_USERPWD, basicauth); + tracepoint(diary, debug_string, "curl_easy_perform() basicauth", basicauth); + } else { + char bearer_token[strlen("Authorization: Bearer ") + strlen(access_token) + 1]; + sprintf(bearer_token, "Authorization: Bearer %s", access_token); + header = curl_slist_append(header, bearer_token); } - // construct header fields - struct curl_slist *header = NULL; - char bearer_token[strlen("Authorization: Bearer ") + strlen(access_token) + 1]; - sprintf(bearer_token, "Authorization: Bearer %s", access_token); char depth_header[strlen("Depth: 0") + 1]; sprintf(depth_header, "Depth: %i", depth); header = curl_slist_append(header, depth_header); - header = curl_slist_append(header, bearer_token); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header); + if (content_type != "" && content_type != NULL) { + char contentType[strlen("Content-Type: ") + strlen(content_type) + 1]; + sprintf(contentType, "Content-Type: %s", content_type); + header = curl_slist_append(header, contentType); + } + // set postfields, if any if (postfields != NULL) { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields); @@ -459,6 +469,9 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields res = curl_easy_perform(curl); + long response_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + // free the memory used for the curl slist curl_slist_free_all(header); @@ -466,6 +479,7 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields if (res != CURLE_OK) { tracepoint(diary, error_string, "curl_easy_perform() failed in caldav_req()", curl_easy_strerror(res)); + tracepoint(diary, debug_long, "curl_easy_perform() response code", response_code); free(caldav_resp.memory); return NULL; } @@ -476,7 +490,7 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields /* Return current user principal from CalDAV XML response */ char* parse_caldav_current_user_principal(char* xml) { - char* xml_key_pos = strstr(xml, "<D:current-user-principal>"); + char* xml_key_pos = strstr(xml, "current-user-principal>"); // this XML does not contain a user principal at all if (xml_key_pos == NULL) { return NULL; @@ -496,32 +510,53 @@ char* parse_caldav_current_user_principal(char* xml) { return tok; } +/* Return home set uri from CalDAV home set XML response */ +char* parse_home_set(char* xml) { + char* homeset_needle = "<calendar-home-set>"; + + char* homeset_pos = strstr(xml, homeset_needle); + if (homeset_pos == NULL) { + tracepoint(diary, debug, "parse_home_set() homeset_pos was null"); + return NULL; + } + tracepoint(diary, debug_string, "parse_home_set() homeset_pos", homeset_pos); + + char* href = strrstr(homeset_pos, "href>/"); + if (href != NULL) { + tracepoint(diary, debug_string, "parse_home_set() href", href); + href = strtok(href, "<"); + tracepoint(diary, debug_string, "parse_home_set() href strtok", href); + if (href != NULL) { + href = strchr(href, '>'); + href++; // cut > + } + return href; + } + return NULL; +} + /* Return calendar uri from CalDAV home set XML response */ char* parse_caldav_calendar(char* xml, char* calendar) { - char displayname_needle[strlen(calendar) + strlen("<D:displayname></D:displayname>")]; - sprintf(displayname_needle, "<D:displayname>%s</D:displayname>", calendar); + char displayname_needle[strlen(calendar) + strlen("displayname></") + 1]; + sprintf(displayname_needle, "displayname>%s</", calendar); char* displayname_pos = strstr(xml, displayname_needle); // this XML multistatus response does not contain the users calendar if (displayname_pos == NULL) { + tracepoint(diary, debug, "parse_caldav_calenar() displayname_pos was null"); return NULL; } - - // <D:response> - // <D:href>/caldav/v2/2fcv7j5mf38o5u2kg5tar4baao%40group.calendar.google.com/events/</D:href> - // <D:propstat> - // <D:status></D:status> - // <D:prop> - // <D:displayname>diary</D:displayname> - // </D:prop> - // </D:propstat> - // </D:response> + tracepoint(diary, debug_string, "parse_caldav_calenar() displayname_pos", displayname_pos); // shorten multistatus response and find last hyperlink *displayname_pos= '\0'; - char* href = strrstr(xml, "<D:href>"); + + tracepoint(diary, debug_string, "parse_caldav_calenar() shortened multistatus response", xml); + char* href = strrstr(xml, "href>/"); if (href != NULL) { + tracepoint(diary, debug_string, "parse_caldav_calenar() last href", href); href = strtok(href, "<"); // :href>/caldav/v2/aaa%40group.calendar.google.com/events/ + tracepoint(diary, debug_string, "parse_caldav_calenar() last href strtok", href); if (href != NULL) { href = strchr(href, '>'); href++; // cut > @@ -591,7 +626,7 @@ void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar strcat(calendar_uri, uid); strcat(calendar_uri, ".ics"); - char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0, basicauth); + char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0, basicauth, "text/calendar"); fclose(fp); free(folded_descr); free(descr); @@ -621,10 +656,10 @@ int caldav_sync(struct tm* date, char* info_txt; - bool google_oath_enabled = !(strcmp(CONFIG.google_clientid, "") == 0 || strcmp(CONFIG.google_clientid, "") == 0); + bool google_oauth_enabled = !(strcmp(CONFIG.google_clientid, "") == 0 || strcmp(CONFIG.google_clientid, "") == 0); bool basicauth_enabled = !(strcmp(CONFIG.caldav_username, "") == 0 || strcmp(CONFIG.caldav_password, "") == 0); - if (strcmp(CONFIG.caldav_server, "") == 0 || strcmp(CONFIG.caldav_calendar, "") == 0 || !(google_oath_enabled && basicauth_enabled)) { + if (strcmp(CONFIG.caldav_server, "") == 0 || strcmp(CONFIG.caldav_calendar, "") == 0 || !(google_oauth_enabled || basicauth_enabled)) { tracepoint(diary, debug_string, "CONFIG.caldav_server", CONFIG.caldav_server); tracepoint(diary, debug_string, "CONFIG.caldav_calendar", CONFIG.caldav_calendar); tracepoint(diary, debug_string, "CONFIG.caldav_username", CONFIG.caldav_username); @@ -643,7 +678,7 @@ int caldav_sync(struct tm* date, wclear(header); } - if (google_oath_enabled) { + if (google_oauth_enabled) { // fetch existing API tokens from file char* tokfile = read_tokenfile(); free(tokfile); @@ -677,17 +712,18 @@ int caldav_sync(struct tm* date, // check if we can use the token from the tokenfile - char* user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled); + char* user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled, "application/xml"); + tracepoint(diary, debug_string, "User principal", user_principal); if (user_principal == NULL) { // The principal could not be fetched tracepoint(diary, debug, "Unable to fetch principal"); - if (google_oath_enabled) { + if (google_oauth_enabled) { // get new acess token with refresh token tracepoint(diary, debug, "Fetching access token with refresh token"); get_access_token(NULL, NULL, true); // Retry request for event with new token - user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled); + user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled, "application/xml"); } } @@ -712,16 +748,42 @@ int caldav_sync(struct tm* date, } char* current_user_principal = parse_caldav_current_user_principal(user_principal); + tracepoint(diary, debug_string, "Current user principal", current_user_principal); // get the home-set of the user char uri[300]; - sprintf(uri, "%s%s", GOOGLE_API_URI, current_user_principal); + sprintf(uri, "%s%s", CONFIG.caldav_server, current_user_principal); // free memory allocated by curl request free(user_principal); - char* home_set = caldav_req(date, uri, "PROPFIND", "", 1, basicauth_enabled); + + char* homeset_request = "<d:propfind xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" + " <d:prop>" + " <c:calendar-home-set />" + " </d:prop>" + "</d:propfind>"; + + char* home_set = caldav_req(date, uri, "PROPFIND", homeset_request, 0, basicauth_enabled, "application/xml"); + tracepoint(diary, debug_string, "Home set xml", home_set); + + // parse home set from xml response + char* home_set_parsed = parse_home_set(home_set); + sprintf(uri, "%s%s", CONFIG.caldav_server, home_set_parsed); + + char* calendar_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:c='urn:ietf:params:xml:ns:caldav'>" + " <d:prop>" + " <d:resourcetype />" + " <d:displayname />" + " <cs:getctag />" + " <c:supported-calendar-component-set />" + " </d:prop>" + "</d:propfind>"; + + // get calendar from home-set + char* calendar_xml = caldav_req(date, uri, "PROPFIND", calendar_request, 1, basicauth_enabled, "application/xml"); + tracepoint(diary, debug_string, "Calendar set xml", calendar_xml); // get calendar URI from the home-set - char* calendar_href = parse_caldav_calendar(home_set, CONFIG.caldav_calendar); + char* calendar_href = parse_caldav_calendar(calendar_xml, CONFIG.caldav_calendar); char* xml_filter = "<c:calendar-query xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" "<d:prop><c:calendar-data/></d:prop>" @@ -730,32 +792,23 @@ int caldav_sync(struct tm* date, "<c:time-range start='%s' end='%s'/></c:comp-filter>" "</c:comp-filter></c:filter></c:calendar-query>"; - // construct next day from date+1 - time_t date_time = mktime(date); - struct tm* next_day = localtime(&date_time); - next_day->tm_mday++; - mktime(next_day); - char dstr_cursor[30]; - char dstr_next_day[30]; - char* format = "%Y%m%dT000000Z"; strftime(dstr_cursor, sizeof dstr_cursor, format, date); - strftime(dstr_next_day, sizeof dstr_next_day, format, next_day); char caldata_postfields[strlen(xml_filter)+50]; sprintf(caldata_postfields, xml_filter, dstr_cursor, - dstr_next_day); + dstr_cursor); // fetch event for the cursor date - sprintf(uri, "%s%s", GOOGLE_API_URI, calendar_href); + sprintf(uri, "%s%s", CONFIG.caldav_server, calendar_href); - char* event = caldav_req(date, uri, "REPORT", caldata_postfields, 0, basicauth_enabled); + char* event = caldav_req(date, uri, "REPORT", caldata_postfields, 1, basicauth_enabled, "application/xml"); // todo: warn if multiple events, // multistatus has more than just one caldav:calendar-data elements // currently, the code below will just extract the first occurance of - // LAST-MODIFIED, UID and DESCRIPTION (from the first event) + // DTSTAMP, UID and DESCRIPTION (from the first event) if (event == NULL) { // Event not found. The curl request probably failed due to one of the missing input parameters: @@ -791,17 +844,19 @@ int caldav_sync(struct tm* date, time_t remote_date = 0; long search_pos = 0; - // check remote LAST-MODIFIED:20210521T212441Z of remote event - char* remote_last_mod = extract_ical_field(event, "LAST-MODIFIED", &search_pos, false); + // check remote DTSTAMP of remote event + char* remote_dtstamp = extract_ical_field(event, "DTSTAMP", &search_pos, false); char* remote_uid = extract_ical_field(event, "UID", &search_pos, false); + tracepoint(diary, debug_string, "event", event); + tracepoint(diary, debug_string, "DTSTAMP field", remote_dtstamp); // init remote date to 1970, assume remote file does not exist remote_datetime = gmtime(&epoch_zero); - if (remote_last_mod != NULL) { + if (remote_dtstamp != NULL) { // if remote date does exist, set date to the correct timestamp - strptime(remote_last_mod, "%Y%m%dT%H%M%SZ", remote_datetime); - free(remote_last_mod); + strptime(remote_dtstamp, "%Y%m%dT%H%M%SZ", remote_datetime); + free(remote_dtstamp); } remote_date = mktime(remote_datetime); @@ -829,9 +884,9 @@ int caldav_sync(struct tm* date, if (remote_uid) { // purge any existing daily calendar entries on the remote side char event_uri[300]; - sprintf(event_uri, "%s%s%s.ics", GOOGLE_API_URI, calendar_href, remote_uid); + sprintf(event_uri, "%s%s%s.ics", CONFIG.caldav_server, calendar_href, remote_uid); tracepoint(diary, debug_string, "Event URI for DELETE request", event_uri); - char* response = caldav_req(date, event_uri, "DELETE", NULL, 0, basicauth_enabled); + char* response = caldav_req(date, event_uri, "DELETE", NULL, 0, basicauth_enabled, ""); free(response); } diff --git a/src/diary-tp.h b/src/diary-tp.h @@ -121,6 +121,19 @@ TRACEPOINT_EVENT( TRACEPOINT_EVENT( diary, + debug_long, + TP_ARGS( + char*, msg_arg, + long, n_arg + ), + TP_FIELDS( + ctf_string(msg, msg_arg) + ctf_integer(long, n, n_arg) + ) +) + +TRACEPOINT_EVENT( + diary, debug_sizet, TP_ARGS( char*, msg_arg, @@ -179,6 +192,7 @@ TRACEPOINT_LOGLEVEL(diary, debug, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_string, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_int, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_double, TRACE_DEBUG) +TRACEPOINT_LOGLEVEL(diary, debug_long, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_sizet, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, debug_tm, TRACE_DEBUG) TRACEPOINT_LOGLEVEL(diary, warning_string, TRACE_WARNING) diff --git a/src/utils.c b/src/utils.c @@ -165,7 +165,7 @@ char* unfold(const char* str) { } strcpy(strcp, str); - char* res = strtok(strcp, "\n"); + char* res = strtok(strcp, "\r\n"); if (res == NULL) { tracepoint(diary, debug, "No more lines in multiline string, stop unfolding"); @@ -224,7 +224,7 @@ char* unfold(const char* str) { char* extract_ical_field(const char* ics, char* key, long* start_pos, bool multiline) { regex_t re; regmatch_t pm[1]; - char key_regex[strlen(key) + 1]; + char key_regex[strlen(key) + 2]; sprintf(key_regex, "^%s", key); if (regcomp(&re, key_regex, 0) != 0) {