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:
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) {