/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ #include #include #include #include #include #include #include #include "webdav_cookie.h" #include "webdav_utils.h" // ************** // Manage cookies // ************** int lock_cookies(void); int unlock_cookies(void); void add_cookie(WEBDAV_COOKIE *newCookie); boolean_t removeMatchingCookie(WEBDAV_COOKIE *aCookie); void list_insert_cookie(WEBDAV_COOKIE *newCookie); void list_remove_cookie(WEBDAV_COOKIE *aCookie); WEBDAV_COOKIE *dequeueCookie(void); WEBDAV_COOKIE *find_cookie(WEBDAV_COOKIE *aCookie); boolean_t cookies_match(WEBDAV_COOKIE *cookie1, WEBDAV_COOKIE *cookie2); boolean_t checkCookieExpired(WEBDAV_COOKIE *aCookie); // ************* // Parse cookies // ************* typedef enum token_result{ COOKIE_EOF = 0, // reached EOF COOKIE_PARSE_ERR = 1, // unexpected parse error COOKIE_NAME = 2, // name as in "name = value" (delimited by '=') COOKIE_VALUE = 3, // value, as in "name = value" (delimited by ';' or '"') COOKIE_DELIMITER = 4 // end of current cookie, found ',' as in "cookie=name; attr=value, cookie2=name2" } TOKEN_RESULT; typedef enum cookie_parse_state { ST_COOKIE_NAME = 0, ST_COOKIE_VAL = 1, ST_ATTR_NAME = 2, ST_PATH_VAL = 3, ST_DOMAIN_VAL = 4, ST_EXPIRES_VAL = 5, ST_MAXAGE_VAL = 6 } PARSE_COOKIE_STATE; #define KEEP_QUOTED 0 WEBDAV_COOKIE *nextCookieFromHeader(CFStringRef cookieHeader, CFIndex headerLen, CFIndex startPosition, CFIndex *nextPosition); CFStringRef nextToken(CFStringRef str, CFIndex strLen, CFIndex startPos, TOKEN_RESULT *result, CFIndex *nextPosition); boolean_t isWeekday(CFStringRef str, CFIndex strLen, CFIndex position); void skipWhiteSpace(CFStringRef str, CFIndex strLen, CFIndex *position); void skipWhiteSpaceReverse(CFStringRef str, CFIndex startPosition, CFIndex *position); boolean_t findNextSeparator(CFStringRef str, CFIndex strLen, CFIndex startPos, CFIndex *foundAt, UniChar *ch); boolean_t findCharacter(CFStringRef str, CFIndex strLen, CFIndex startPos, CFIndex *foundAt, UniChar ch); CFStringRef cleanDomainName(CFStringRef inStr); CFStringRef nextDomainComponent(CFStringRef inStr, CFIndex strLen, CFIndex startPos, CFIndex *nextStartPos); boolean_t isLetterDigitHyphen(UniChar ch); // helpers boolean_t path2InPath1(const char *path1, const char *path2); void free_cookie_fields(WEBDAV_COOKIE *cookie); void printCookie(WEBDAV_COOKIE *aCookie); CFStringRef cookiePathFromURL(CFURLRef url); boolean_t doesDomainMatch(CFStringRef domainStr, CFStringRef aStr); CFStringRef cleanDomainName(CFStringRef inStr); boolean_t is_ip_address_str(CFStringRef hostStr); boolean_t is_ip_address(const char *host); // global array of cookies #define WEBDAV_MAX_COOKIES 10 WEBDAV_COOKIE *cookie_head, *cookie_tail; uint32_t cookie_count; pthread_mutex_t cookie_lock; extern int gSecureConnection; extern CFURLRef gBaseURL; /* the base URL for this mount */ extern CFStringRef gBasePath; /* the base path (from gBaseURL) for this mount */ extern char gBasePathStr[MAXPATHLEN]; /* gBasePath as a c-string */ // main entry point for incoming cookie void handle_cookies(CFStringRef str, CFHTTPMessageRef message) { WEBDAV_COOKIE *aCookie; CFURLRef url; CFStringRef urlPathStr, domainStr, tmpStr; CFIndex pos1, pos2, len; boolean_t done; urlPathStr = NULL; url = NULL; if ( str == NULL ) { goto err_out; } len = CFStringGetLength(str); if (!len) { goto err_out; } done= false; pos1 = 0; while (done == false) { aCookie = nextCookieFromHeader(str, len, pos1, &pos2); if (aCookie == NULL) { // all done goto err_out; } // ****************** // 1. Cookie Expired? // ****************** if (checkCookieExpired(aCookie) == true) { syslog(LOG_DEBUG, "%s: COOKIE NOT ACCEPTED %s=%s, expired\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str); // Delete existing cookie if we have it lock_cookies(); removeMatchingCookie(aCookie); unlock_cookies(); // Free this expired cookie free_cookie_fields(aCookie); free (aCookie); // on to next cookie goto skip_cookie; } // ************************* // 2. Check Secure attribute // ************************* if ((aCookie->cookie_secure == true) && (gSecureConnection == false)) { syslog(LOG_DEBUG, "%s: COOKIE NOT ACCEPTED %s=%s, requires secure connection\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str); // Free this expired cookie free_cookie_fields(aCookie); free (aCookie); // on to next cookie goto skip_cookie; } // ************************* // 3. Check domain attribute // ************************* url = CFHTTPMessageCopyRequestURL(message); if (url == NULL) { // nothing we can do, forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } tmpStr = CFURLCopyHostName(url); CFRelease(url); if (tmpStr == NULL) { // nothing we can do, forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } // clean domain str domainStr = cleanDomainName(tmpStr); if (domainStr == NULL) { syslog(LOG_DEBUG, "%s: COOKIE NOT ACCEPTED %s=%s, cleanDomainName error\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str); // nothing we can do, forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } if ( (is_ip_address_str(domainStr) == true) || (aCookie->cookie_domain == NULL)) { // URL has an ip address instead of hostname, // have to set cookie domain to URL ip address if (aCookie->cookie_domain != NULL) { CFRelease(aCookie->cookie_domain); aCookie->cookie_domain = NULL; } if (aCookie->cookie_domain_str != NULL) { free (aCookie->cookie_domain_str); aCookie->cookie_domain_str = NULL; } aCookie->cookie_domain = domainStr; aCookie->cookie_domain_str = createUTF8CStringFromCFString(domainStr); if (aCookie->cookie_domain_str == NULL) { // have to punt free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } } else if (doesDomainMatch(aCookie->cookie_domain, domainStr) == false) { // This cookie will never be sent out, because the cookie domain // does not "domain match" domain of hostname syslog(LOG_DEBUG, "%s: cookie domain mismatch %s=%s, cookie domain: %s, host: %s\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str, aCookie->cookie_domain_str, gBasePathStr); // have to punt free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } // *********************** // 4. Check path attribute // *********************** if (aCookie->cookie_path == NULL) { // Assign path from request url as path attribute for this cookie if (message == NULL) { // nothing we can do, forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } url = CFHTTPMessageCopyRequestURL(message); if (url == NULL) { // nothing we can do, forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } urlPathStr = cookiePathFromURL(url); CFRelease(url); if (urlPathStr == NULL) { // nothing we can do, forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } aCookie->cookie_path = urlPathStr; aCookie->cookie_path_str = createUTF8CStringFromCFString(urlPathStr); } else { if ((path2InPath1(gBasePathStr, aCookie->cookie_path_str) != true) && (path2InPath1(aCookie->cookie_path_str, gBasePathStr) != true)) { syslog(LOG_DEBUG, "%s: COOKIE NOT ACCEPTED %s=%s, path mismatch: gBasePath: %s, cookie_path: %s\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str, gBasePathStr, aCookie->cookie_path_str); // path attribute is not a subpath of gBaseURL path, we will never send // it out, so forget this cookie free_cookie_fields(aCookie); free(aCookie); // on to the next cookie goto skip_cookie; } } // add the new cookie add_cookie(aCookie); skip_cookie: pos1 = pos2; } err_out: return; } void add_cookie_headers(CFHTTPMessageRef message, CFURLRef url) { CFStringRef urlPathStr; CFMutableStringRef cookieStr; WEBDAV_COOKIE *aCookie; char *cpath; cpath = NULL; cookieStr = NULL; urlPathStr = NULL; if (!cookie_count) { goto err_out; } urlPathStr = cookiePathFromURL(url); if (urlPathStr == NULL) { syslog(LOG_DEBUG, "%s: no path from urlPathStr\n", __FUNCTION__); goto err_out; } cpath = createUTF8CStringFromCFString(urlPathStr); if (cpath == NULL) { goto err_out; } lock_cookies(); aCookie = cookie_head; while (aCookie != NULL) { if ((aCookie->cookie_secure == true) && (gSecureConnection != true)) { // Don't have a secure connection, and this cookie requires one syslog(LOG_DEBUG, "%s: NO MATCH cookie: %s=%s requires secure connection\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str); goto skip_cookie; } if (path2InPath1(aCookie->cookie_path_str, cpath) == true) { // add this cookie to outgoing message if (cookieStr == NULL) { cookieStr = CFStringCreateMutable(kCFAllocatorDefault, 0); if (cookieStr == NULL) { unlock_cookies(); goto err_out; } CFStringAppend(cookieStr, aCookie->cookie_header); } else { CFStringAppend(cookieStr, CFSTR("; ")); CFStringAppend(cookieStr, aCookie->cookie_header); } } else { syslog(LOG_DEBUG, "%s: cookie: %s=%s, Failed path-match, cookie_path: %s url path: %s\n", __FUNCTION__, aCookie->cookie_name_str, aCookie->cookie_val_str, aCookie->cookie_path_str, cpath); } skip_cookie: aCookie = aCookie->next; } unlock_cookies(); if (cookieStr != NULL) { CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Cookie"), cookieStr); } err_out: if (urlPathStr != NULL) CFRelease(urlPathStr); if (cookieStr != NULL) CFRelease(cookieStr); if (cpath != NULL) free (cpath); return; } void purge_expired_cookies(void) { WEBDAV_COOKIE *aCookie, *nextCookie; time_t now; lock_cookies(); if (cookie_head == 0) { // nothing to purge unlock_cookies(); return; } now = time(NULL); aCookie = cookie_head; while (aCookie != NULL) { nextCookie = aCookie->next; if (aCookie->has_expire_time == true) { if (now >= aCookie->cookie_expire_time) { // this cookie is expired list_remove_cookie(aCookie); free_cookie_fields(aCookie); free(aCookie); } } aCookie = nextCookie; } unlock_cookies(); } void add_cookie(WEBDAV_COOKIE *newCookie) { // Remove any matching cookie lock_cookies(); removeMatchingCookie(newCookie); list_insert_cookie(newCookie); unlock_cookies(); } boolean_t removeMatchingCookie(WEBDAV_COOKIE *aCookie) { WEBDAV_COOKIE *anotherCookie; boolean_t didRemove; didRemove = false; if (cookie_head == 0) goto out; anotherCookie = find_cookie(aCookie); if (anotherCookie != NULL) { list_remove_cookie(anotherCookie); free_cookie_fields(anotherCookie); free(anotherCookie); didRemove = true; } out: return (didRemove); } void list_remove_cookie(WEBDAV_COOKIE *aCookie) { if (aCookie->prev == NULL) { // head position cookie_head = aCookie->next; if (cookie_head == NULL) { cookie_tail = NULL; cookie_count = 0; } else { cookie_head->prev = NULL; cookie_count--; } } else if (aCookie->next == NULL) { // tail position cookie_tail = aCookie->prev; cookie_tail->next = NULL; cookie_count--; } else { // somewhere in the middle aCookie->prev->next = aCookie->next; aCookie->next->prev = aCookie->prev; cookie_count--; } } void list_insert_cookie(WEBDAV_COOKIE *newCookie) { WEBDAV_COOKIE *aCookie; if (cookie_count >= WEBDAV_MAX_COOKIES) { // Remove the oldest cookie aCookie = dequeueCookie(); if (aCookie != NULL) { free_cookie_fields(aCookie); free(aCookie); } } if (cookie_head == NULL) { // empty list cookie_head = newCookie; cookie_tail = newCookie; newCookie->prev = NULL; newCookie->next = NULL; } else if (cookie_head == cookie_tail) { // only one in list cookie_head->next = newCookie; cookie_tail = newCookie; newCookie->prev = cookie_head; } else { // more than one in list cookie_tail->next = newCookie; newCookie->prev = cookie_tail; cookie_tail = newCookie; } cookie_count++; return; } WEBDAV_COOKIE *dequeueCookie(void) { WEBDAV_COOKIE *aCookie = NULL; if (cookie_head == NULL) { // empty cookie_count = 0; goto out; } if (cookie_head == cookie_tail) { // only one in list aCookie = cookie_head; cookie_head = NULL; cookie_tail = NULL; cookie_count = 0; goto out; } // more than one in list aCookie = cookie_tail; cookie_tail = aCookie->prev; cookie_tail->next = NULL; if (cookie_count) cookie_count--; out: return (aCookie); } WEBDAV_COOKIE *find_cookie(WEBDAV_COOKIE *aCookie) { WEBDAV_COOKIE *someCookie, *matchCookie; matchCookie = NULL; someCookie = cookie_head; while (someCookie != NULL) { if (cookies_match(aCookie, someCookie) == true) { matchCookie = someCookie; break; } someCookie = someCookie->next; } return (matchCookie); } boolean_t cookies_match(WEBDAV_COOKIE *cookie1, WEBDAV_COOKIE *cookie2) { size_t len1, len2; boolean_t matched = false; if (cookie1 == NULL || cookie2 == NULL) { goto out; } // (1) Check cookie names if (cookie1->cookie_name_str == NULL || cookie2->cookie_name_str == NULL) { goto out; } len1 = strlen(cookie1->cookie_name_str); len2 = strlen(cookie2->cookie_name_str); if (len1 != len2) { goto out; } if(strncmp(cookie1->cookie_name_str, cookie1->cookie_name_str, len1) != 0) { goto out; } // (2) Check cookie paths if (cookie1->cookie_path_str != NULL) { if (cookie2->cookie_path_str == NULL){ goto out; } len1 = strlen(cookie1->cookie_path_str); len2 = strlen(cookie2->cookie_path_str); if (len1 != len2) { goto out; } if (strncmp(cookie1->cookie_path_str, cookie2->cookie_path_str, len1) != 0) { goto out; } } else { if (cookie2->cookie_path_str != NULL) { goto out; } } // (3) Check cookie domains if (cookie1->cookie_domain_str != NULL) { if (cookie2->cookie_domain_str == NULL){ goto out; } len1 = strlen(cookie1->cookie_domain_str); len2 = strlen(cookie2->cookie_domain_str); if (len1 != len2) { goto out; } if (strncmp(cookie1->cookie_domain_str, cookie2->cookie_domain_str, len1) != 0) { goto out; } } else { if (cookie2->cookie_domain_str != NULL) { goto out; } } // If we made it this far, they match matched = true; out: return (matched); } boolean_t checkCookieExpired(WEBDAV_COOKIE *aCookie) { boolean_t hasExpired; time_t now; hasExpired = false; now = time(NULL); if (aCookie->has_expire_time != true) { goto out; } if ( (aCookie->cookie_expire_time == 0) || (aCookie->cookie_expire_time < now)) { hasExpired = true; } out: return (hasExpired); } // *********************** // *** PARSING COOKIES *** // *********************** WEBDAV_COOKIE *nextCookieFromHeader(CFStringRef cookieHeader, CFIndex headerLen, CFIndex startPosition, CFIndex *nextPosition) { WEBDAV_COOKIE *nextCookie; CFStringRef tokenStr, tmpStr; CFIndex pos1, pos2, len; CFRange range; TOKEN_RESULT res; PARSE_COOKIE_STATE st; char *str; boolean_t done; nextCookie = NULL; st = ST_COOKIE_NAME; if (startPosition >= headerLen) { goto err_out; } nextCookie = malloc(sizeof(WEBDAV_COOKIE)); if (nextCookie == NULL) { goto err_out; } bzero(nextCookie, sizeof(WEBDAV_COOKIE)); pos1 = startPosition; done = false; while (done == false) { tokenStr = nextToken(cookieHeader, headerLen, pos1, &res, &pos2); switch (res) { case COOKIE_NAME: switch (st) { case ST_COOKIE_NAME: nextCookie->cookie_name = tokenStr; nextCookie->cookie_name_str = createUTF8CStringFromCFString(tokenStr); st = ST_COOKIE_VAL; break; case ST_COOKIE_VAL: // Shouldn't happen CFRelease(tokenStr); goto err_out; break; case ST_ATTR_NAME: if (CFStringCompare(tokenStr, CFSTR("Path"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // Path? nextCookie->cookie_path = tokenStr; nextCookie->cookie_path_str = createUTF8CStringFromCFString(tokenStr); st = ST_PATH_VAL; } else if (CFStringCompare(tokenStr, CFSTR("Domain"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // Domain nextCookie->cookie_domain = tokenStr; nextCookie->cookie_domain_str = createUTF8CStringFromCFString(tokenStr); st = ST_DOMAIN_VAL; } else if (CFStringCompare(tokenStr, CFSTR("Expires"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // Expires st = ST_EXPIRES_VAL; } else if (CFStringCompare(tokenStr, CFSTR("Max-Age"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // MaxAge st = ST_MAXAGE_VAL; } else if (CFStringCompare(tokenStr, CFSTR("Secure"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // Secure nextCookie->cookie_secure = true; CFRelease(tokenStr); } else if (CFStringCompare(tokenStr, CFSTR("HttpOnly"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // HttpOnly nextCookie->cookie_httponly = true; CFRelease(tokenStr); } else { CFRelease(tokenStr); } break; case ST_PATH_VAL: CFRelease(tokenStr); goto err_out; break; case ST_DOMAIN_VAL: break; case ST_EXPIRES_VAL: CFRelease(tokenStr); goto err_out; break; case ST_MAXAGE_VAL: CFRelease(tokenStr); goto err_out; break; } break; case COOKIE_VALUE: switch (st) { case ST_COOKIE_NAME: // Shouldn't happen CFRelease(tokenStr); goto err_out; break; case ST_COOKIE_VAL: nextCookie->cookie_val = tokenStr; nextCookie->cookie_val_str = createUTF8CStringFromCFString(tokenStr); st = ST_ATTR_NAME; break; case ST_ATTR_NAME: if (CFStringCompare(tokenStr, CFSTR("Secure"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // Secure nextCookie->cookie_secure = true; CFRelease(tokenStr); } else if (CFStringCompare(tokenStr, CFSTR("HttpOnly"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { // HttpOnly nextCookie->cookie_httponly = true; CFRelease(tokenStr); } else { CFRelease(tokenStr); } break; case ST_PATH_VAL: len = CFStringGetLength(tokenStr); if ( (CFStringHasSuffix(tokenStr, CFSTR("/")) == true) && len > 1) { // remove trailing slash range.length = len - 1; range.location = 0; tmpStr = CFStringCreateWithSubstring(kCFAllocatorDefault, tokenStr, range); CFRelease(tokenStr); tokenStr = tmpStr; } nextCookie->cookie_path = tokenStr; nextCookie->cookie_path_str = createUTF8CStringFromCFString(tokenStr); st = ST_ATTR_NAME; break; case ST_DOMAIN_VAL: // clean it up (convert to lower case, etc...) nextCookie->cookie_domain = cleanDomainName(tokenStr); if (nextCookie->cookie_domain != NULL) { nextCookie->cookie_domain_str = createUTF8CStringFromCFString(nextCookie->cookie_domain); } CFRelease(tokenStr); st = ST_ATTR_NAME; break; case ST_EXPIRES_VAL: nextCookie->has_expire_time = true; nextCookie->cookie_expire_time = DateStringToTime(tokenStr); st = ST_ATTR_NAME; break; case ST_MAXAGE_VAL: nextCookie->has_expire_time = true; str = createUTF8CStringFromCFString(tokenStr); nextCookie->cookie_expire_time = time(NULL) + strtol(str, NULL, 10); st = ST_ATTR_NAME; break; } break; case COOKIE_DELIMITER: done = true; break; case COOKIE_PARSE_ERR: goto err_out; break; case COOKIE_EOF: done = true; break; } pos1 = pos2; } // Make sure we received a cookie name if (nextCookie->cookie_name == NULL) { goto err_out; } // Fix the path if needed if (nextCookie->cookie_path_str != NULL) { if (nextCookie->cookie_path_str[0] != '/') { // no beginning slash, use default path free(nextCookie->cookie_path_str); if (nextCookie->cookie_path != NULL) CFRelease(nextCookie->cookie_path); nextCookie->cookie_path_str = NULL; nextCookie->cookie_path = NULL; } } // Fix domain if needed if (nextCookie->cookie_domain != NULL) { if (is_ip_address_str(nextCookie->cookie_domain) == true) { // Cannot accept ip address as a domain CFRelease(nextCookie->cookie_domain); nextCookie->cookie_domain = NULL; if (nextCookie->cookie_domain_str != NULL) { free (nextCookie->cookie_domain_str); nextCookie->cookie_domain_str = NULL; } } } // Compose the cookie header (for outgoing messages) nextCookie->cookie_header = CFStringCreateMutable(kCFAllocatorDefault, 0); if (nextCookie->cookie_header == NULL) { goto err_out; } CFStringAppend(nextCookie->cookie_header, nextCookie->cookie_name); if (nextCookie->cookie_val != NULL) { CFStringAppend(nextCookie->cookie_header, CFSTR("=")); CFStringAppend(nextCookie->cookie_header, nextCookie->cookie_val); } *nextPosition = pos2; return (nextCookie); err_out: if (nextCookie) { free_cookie_fields(nextCookie); free (nextCookie); } return (NULL); } CFStringRef nextToken(CFStringRef str, CFIndex strLen, CFIndex startPos, TOKEN_RESULT *result, CFIndex *nextPosition) { CFStringRef token; CFIndex pos1, pos2, pos3; CFRange range, range2; UniChar ch, ch2; boolean_t found; token = NULL; *result = COOKIE_PARSE_ERR; if (startPos >= strLen) { *result = COOKIE_EOF; goto out; } // skip any white space pos1 = startPos; skipWhiteSpace(str, strLen, &pos1); if (pos1 >= strLen) { *result = COOKIE_EOF; goto out; } // Find the next separator found = findNextSeparator(str, strLen, pos1, &pos2, &ch); if (found == false) { // Check for ending name or value range.location = pos1; // trim any trailing whitespace skipWhiteSpaceReverse(str, pos2-1, &pos3); pos3++; if (pos3 > pos1) { range.length = pos3 - pos1; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); *result = COOKIE_VALUE; } else { *result = COOKIE_EOF; } *nextPosition = pos2; goto out; } // token = value // ^ ^ // | | // pos1 pos2 // if (ch == '=') { // Handle quoted string ch2 = CFStringGetCharacterAtIndex(str, pos1); if (ch2 == '"') { if (KEEP_QUOTED) { found = findCharacter(str, strLen, pos1 + 1, &pos3, '"'); } else { pos1++; found = findCharacter(str, strLen, pos1, &pos3, '"'); } // Did we get the ending quote? if (found == false) { // parser is confused *result = COOKIE_PARSE_ERR; goto out; } range.location = pos1; range.length = pos3 - pos1; if (KEEP_QUOTED) range.length++; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); if (token == NULL) { *result = COOKIE_PARSE_ERR; goto out; } // skip over '=' *nextPosition = pos2 + 1; *result = COOKIE_NAME; goto out; } // Name is not quoted range.location = pos1; // trim any trailing white skipWhiteSpaceReverse(str, pos2-1, &pos3); pos3++; range.length = pos3 - pos1; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); if (token == NULL) { *result = COOKIE_PARSE_ERR; goto out; } *nextPosition = pos2 + 1; *result = COOKIE_NAME; goto out; } // token = value; // ^ ^ // | | // pos1 pos2 // if (ch == ';') { // handle quoted value ch2 = CFStringGetCharacterAtIndex(str, pos1); if (ch2 == '"') { if (KEEP_QUOTED) { found = findCharacter(str, strLen, pos1 + 1, &pos3, '"'); } else { pos1++; found = findCharacter(str, strLen, pos1, &pos3, '"'); } // Did we get the ending quote? if (found == false) { // parser is confused *result = COOKIE_PARSE_ERR; goto out; } range.location = pos1; range.length = pos3 - pos1; if (KEEP_QUOTED) range.length++; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); if (token == NULL) { *result = COOKIE_PARSE_ERR; goto out; } // skip over '=' *nextPosition = pos2 + 1; *result = COOKIE_VALUE; goto out; } // Value is not quoted range.location = pos1; // trim any trailing white skipWhiteSpaceReverse(str, pos2-1, &pos3); pos3++; range.length = pos3 - pos1; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); if (token == NULL) { *result = COOKIE_PARSE_ERR; goto out; } *nextPosition = pos2 + 1; *result = COOKIE_VALUE; goto out; } if (ch == ',') { // Make sure we haven't found a comma in a quoted string ch2 = CFStringGetCharacterAtIndex(str, pos1); if (ch2 == '"') { if (KEEP_QUOTED) { found = findCharacter(str, strLen, pos1 + 1, &pos3, '"'); } else { pos1++; found = findCharacter(str, strLen, pos1, &pos3, '"'); } // Did we get the ending quote? if (found == false) { // parser is confused *result = COOKIE_PARSE_ERR; goto out; } range.location = pos1; range.length = pos3 - pos1; if (KEEP_QUOTED) range.length++; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); if (token == NULL) { *result = COOKIE_PARSE_ERR; goto out; } // skip over '"' *nextPosition = pos3 + 1; *result = COOKIE_VALUE; goto out; } // Comma was not in a quoted string, check for date string if (isWeekday(str, strLen, pos1) == true) { // expires=Thu, 23 Jun 2011 01:24:00 GMT // expires=Sunday, 06-Nov-94 08:49:37 GMT // ^ ^ // | | // pos1 pos2 // We've got a date string, so find the "next" separator range2.location = pos1; range2.length = strLen - pos1; if (CFStringFindWithOptions(str, CFSTR("GMT"), range2, 0, &range) != true) { // weekday but no "GMT", parser is confused *result = COOKIE_PARSE_ERR; goto out; } // We have a date string pos2 = range.location + 3; range2.location = pos1; range2.length = pos2 - pos1; token = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range2); if (token == NULL) { *result = COOKIE_PARSE_ERR; goto out; } *nextPosition = pos2; *result = COOKIE_VALUE; goto out; } // If it's not a weekday in an Expires attribute, then it is a // delimiter for the next cookie in the header // expires=Sunday, 06-Nov-94 08:49:37 GMT, next_cookie-value; // ^ // | // pos2 *result = COOKIE_DELIMITER; // skip over the comma *nextPosition = pos1 + 1; } out: return (token); } CFStringRef cleanDomainName(CFStringRef inStr) { CFStringRef workStr; CFMutableStringRef domainStr; CFIndex len, pos, nextPos; boolean_t done, gotFirstComponent; workStr = NULL; domainStr = NULL; gotFirstComponent = false; if (inStr == NULL) { goto out; } domainStr = CFStringCreateMutable(kCFAllocatorDefault, 0); if (domainStr == NULL){ goto out; } len = CFStringGetLength(inStr); pos = 0; done = false; while (done == false) { workStr = nextDomainComponent(inStr, len, pos, &nextPos); if (workStr == NULL) break; if (gotFirstComponent == true) { CFStringAppend(domainStr, CFSTR(".")); } else gotFirstComponent = true; CFStringAppend(domainStr, workStr); CFRelease(workStr); pos = nextPos; } if (CFStringGetLength(domainStr) == 0) { // Didn't get anything CFRelease(domainStr); domainStr = NULL; } if (domainStr != NULL) CFStringLowercase(domainStr, CFLocaleGetSystem()); out: return (domainStr); } CFStringRef nextDomainComponent(CFStringRef inStr, CFIndex strLen, CFIndex startPos, CFIndex *nextStartPos) { CFIndex p1, p2, pos; CFStringRef componentStr; CFRange range; UniChar ch; boolean_t found, result; componentStr = NULL; found = false; pos = startPos; // Find first name character while (pos < strLen) { ch = CFStringGetCharacterAtIndex(inStr, pos); result = isLetterDigitHyphen(ch); if (result == true) { found = true; break; } pos++; } if ( found == false) { goto out; } p1 = pos; // Now find end position pos++; found = false; while (pos < strLen) { ch = CFStringGetCharacterAtIndex(inStr, pos); result = isLetterDigitHyphen(ch); if (result == false) { found = true; break; } pos++; } if (pos <= p1) { // Nothing to return goto out; } p2 = pos; // Now make the comoponent string range.location = p1; range.length = p2 - p1; componentStr = CFStringCreateWithSubstring(kCFAllocatorDefault, inStr, range); *nextStartPos = p2; out: return (componentStr); } boolean_t isLetterDigitHyphen(UniChar ch) { if ( ((ch >= 'A') && ( ch <= 'Z')) || ((ch >= 'a') && ( ch <= 'z')) || ((ch >= '0') && ( ch <= '9')) || (ch == '-') ) { return (true); } else { return (false); } } // // Returns true if aStr domain-matches domainStr // per RFC-6265 // boolean_t doesDomainMatch(CFStringRef domainStr, CFStringRef aStr) { boolean_t result; result = false; if (domainStr == NULL || aStr == NULL) goto out; // First, check if they are identical if (CFStringCompare(domainStr, aStr, kCFCompareCaseInsensitive) == kCFCompareEqualTo) { result = true; goto out; } // Check if domainStr is a suffix of aStr if (CFStringHasSuffix(aStr, domainStr) == true) { result = true; goto out; } out: return (result); } boolean_t is_ip_address_str(CFStringRef hostStr) { boolean_t result; char *str; result = false; if (hostStr == NULL) { goto out; } str = createUTF8CStringFromCFString(hostStr); if (str == NULL) { goto out; } result = is_ip_address(str); out: return (result); } boolean_t is_ip_address(const char *host) { struct in_addr inaddr4; struct in6_addr inaddr6; boolean_t result; result = false; if (host == NULL) { goto out; } // Try IPv4 if (inet_pton(AF_INET, host, &inaddr4) == 1) { result = true; goto out; } // Try IPv6 if (inet_pton(AF_INET6, host, &inaddr6) == 1) { result = true; } out: return (result); } boolean_t isWeekday(CFStringRef str, CFIndex strLen, CFIndex position) { CFIndex len; CFStringRef dayStr; boolean_t weekday; CFRange range; weekday = false; dayStr = NULL; if (position >= strLen) { goto out; } len = strLen - position; if (len < 3) { // Cannot be a weekday "Mon", "Tues", etc... goto out; } range.location = position; range.length = len; dayStr = CFStringCreateWithSubstring(kCFAllocatorDefault, str, range); if (dayStr == NULL) { goto out; } if (CFStringHasPrefix(dayStr, CFSTR("Mon")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Tue")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Wed")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Thu")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Fri")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Sat")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Sun")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Monday")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Tuesday")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Wednesday")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Thursday")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Friday")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Saturday")) == true) weekday = true; else if (CFStringHasPrefix(dayStr, CFSTR("Sunday")) == true) weekday = true; out: if (dayStr != NULL) CFRelease(dayStr); return (weekday); } CFStringRef cookiePathFromURL(CFURLRef url) { CFStringRef pathStr, tmpStr; CFRange rangeIn, rangeOut; CFIndex len; Boolean result; pathStr = NULL; tmpStr = NULL; if (url == NULL) { goto out; } tmpStr = CFURLCopyPath(url); if (tmpStr == NULL) { goto out; } len = CFStringGetLength(tmpStr); if (!len) goto out; if (CFStringHasPrefix(tmpStr, CFSTR("/")) == false) { goto out; } if (len == 1) goto out; // Copy the path up to (but not including) the right-most '/' rangeIn.location = 0; rangeIn.length = len; result = CFStringFindWithOptions(tmpStr, CFSTR("/"), rangeIn, kCFCompareBackwards, &rangeOut); if (result == true) { if (rangeOut.location > 0) { rangeIn.location = 0; rangeIn.length = rangeOut.location; pathStr = CFStringCreateWithSubstring(kCFAllocatorDefault, tmpStr, rangeIn); } else { pathStr = tmpStr; tmpStr = NULL; } goto out; } else { pathStr = tmpStr; tmpStr = NULL; goto out; } out: if (tmpStr != NULL) CFRelease(tmpStr); if (pathStr == NULL) { pathStr = CFStringCreateCopy(kCFAllocatorDefault, CFSTR("/")); } return (pathStr); } void skipWhiteSpace(CFStringRef str, CFIndex strLen, CFIndex *position) { CFIndex pos; UniChar c; pos = *position; while (pos < strLen) { c = CFStringGetCharacterAtIndex(str, pos); if ((c > 32) && (c < 127)) { break; } pos++; } *position = pos; } void skipWhiteSpaceReverse(CFStringRef str, CFIndex startPosition, CFIndex *position) { CFIndex pos; UniChar c; pos = startPosition; while (pos > 0) { c = CFStringGetCharacterAtIndex(str, pos); if ((c > 32) && (c < 127)) { break; } pos--; } *position = pos; } boolean_t findNextSeparator(CFStringRef str, CFIndex strLen, CFIndex startPos, CFIndex *foundAt, UniChar *ch) { CFIndex pos; UniChar c; boolean_t foundit; foundit = false; pos = startPos; // Scan for the next separator while (pos < strLen) { c = CFStringGetCharacterAtIndex(str, pos); switch (c) { case ',': case '=': case ';': foundit = true; *ch = c; goto out; break; default: break; } pos++; } out: *foundAt = pos; return (foundit); } boolean_t findCharacter(CFStringRef str, CFIndex strLen, CFIndex startPos, CFIndex *foundAt, UniChar ch) { CFIndex pos; UniChar c; boolean_t foundit; foundit = false; pos = startPos; // Scanning while (pos < strLen) { c = CFStringGetCharacterAtIndex(str, pos); if (c == ch) { foundit = true; *foundAt = pos; break; } pos++; } return (foundit); } void free_cookie_fields(WEBDAV_COOKIE *cookie) { if (cookie != NULL) { if (cookie->cookie_header != NULL) CFRelease(cookie->cookie_header); if (cookie->cookie_name != NULL) CFRelease(cookie->cookie_name); if (cookie->cookie_name_str != NULL) free(cookie->cookie_name_str); if (cookie->cookie_val != NULL) CFRelease(cookie->cookie_val); if (cookie->cookie_val_str != NULL) free(cookie->cookie_val_str); if (cookie->cookie_path != NULL) CFRelease(cookie->cookie_path); if (cookie->cookie_path_str != NULL) free(cookie->cookie_path_str); if (cookie->cookie_domain != NULL) CFRelease(cookie->cookie_domain); if (cookie->cookie_domain_str != NULL) free(cookie->cookie_domain_str); } } void cookies_init(void) { pthread_mutexattr_t mutexattr; pthread_mutexattr_init(&mutexattr); pthread_mutex_init(&cookie_lock, &mutexattr); cookie_head = NULL; cookie_tail = NULL; cookie_count = 0; } // Returns TRUE if path2 is enclosed in path1. // // Example: path1 = /a/b/c // // path2 Return Value // ------- ------------ // /a FALSE // /a/b FALSE // /a/b/c TRUE // /a/b/c/d TRUE // boolean_t path2InPath1(const char *path1, const char *path2) { size_t path1Len, path2Len, i; Boolean ret; ret = false; if (path1 == NULL || path2 == NULL) goto out; path1Len = strlen(path1); path2Len = strlen(path2); // check for zero string len if (path1Len == 0 || path2Len == 0) { goto out; } // make sure we have absolute paths here if (path1[0] != '/' || path2[0] != '/') { goto out; } // If path1 is '/', then it includes all subpaths if (path1Len == 1) { ret = true; goto out; } // strip trailing slashes if (path1[path1Len - 1] == '/') path1Len--; if (path2[path2Len - 1] == '/') path2Len--; if (path1Len > path2Len) { goto out; } for (i = 0; i < path1Len; i++) { if (path1[i] != path2[i]) { goto out; } } // if we got here then path2 is contained in path1 ret = true; out: return (ret); } /*****************************************************************************/ int lock_cookies(void) { int error; error = pthread_mutex_lock(&cookie_lock); return (error); } /*****************************************************************************/ int unlock_cookies(void) { int error; error = pthread_mutex_unlock(&cookie_lock); return (error); } /*****************************************************************************/ void dump_cookies(struct webdav_request_cookies *req) { WEBDAV_COOKIE *aCookie; if (req == NULL) { syslog(LOG_DEBUG, "%s: req is null\n", __FUNCTION__); } lock_cookies(); syslog(LOG_ERR, "%s: Cookie count: %u\n", __FUNCTION__, cookie_count); aCookie = cookie_head; while(aCookie != NULL) { printCookie(aCookie); aCookie = aCookie->next; } unlock_cookies(); } void reset_cookies(struct webdav_request_cookies *req) { WEBDAV_COOKIE *aCookie, *nextCookie; uint32_t num; if (req == NULL) { syslog(LOG_DEBUG, "%s: req is null\n", __FUNCTION__); } lock_cookies(); num = 0; aCookie = cookie_head; while(aCookie != NULL) { nextCookie = aCookie->next; list_remove_cookie(aCookie); syslog(LOG_ERR, "%s: Removing cookie: %s\n", __FUNCTION__, aCookie->cookie_name_str); free_cookie_fields(aCookie); free (aCookie); num++; aCookie = nextCookie; } unlock_cookies(); syslog(LOG_ERR, "%s: Removed %u cookies\n", __FUNCTION__, num); } /*****************************************************************************/ void printCookie(WEBDAV_COOKIE *aCookie) { time_t now; now = time(NULL); if (aCookie->cookie_val_str == NULL) { syslog(LOG_ERR, "Cookie: '%s'\n", aCookie->cookie_name_str); } else { syslog(LOG_ERR, "Cookie: %s='%s'\n", aCookie->cookie_name_str, aCookie->cookie_val_str); } if (aCookie->cookie_path_str != NULL) { syslog(LOG_ERR, "Path: %s\n", aCookie->cookie_path_str); } if (aCookie->cookie_domain_str != NULL) { syslog(LOG_ERR, "Domain: %s\n", aCookie->cookie_domain_str); } if (aCookie->has_expire_time == TRUE) { now = time(NULL); syslog(LOG_ERR, "Expires @: %ld (current %ld)\n", aCookie->cookie_expire_time, now); } if (aCookie->cookie_secure == TRUE) syslog(LOG_ERR, "Secure\n"); if (aCookie->cookie_httponly == TRUE) syslog(LOG_ERR, "HttpOnly\n"); } /*****************************************************************************/