1/* 2Copyright (c) 2000-2013 Apple Inc. All Rights Reserved. 3 4This file contains Original Code and/or Modifications of Original Code 5as defined in and that are subject to the Apple Public Source License 6Version 2.0 (the 'License'). You may not use this file except in 7compliance with the License. Please obtain a copy of the License at 8http://www.opensource.apple.com/apsl/ and read it before using this 9file. 10 11The Original Code and all software distributed under the License are 12distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 13EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 14INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 15FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 16Please see the License for the specific language governing rights and 17limitations under the License. 18*/ 19 20/* 21 * mod_hfs_apple Apache module (enforce casing in URLs which need it) 22 * 23 * When a <Directory> statement is found in the configuration file (this 24 * discussion does not apply if .htaccess files are used instead) then 25 * its directory path is supposed to apply to any URL which URI uses 26 * that directory. In other words, a <Directory> statement usually 27 * defines some restrictions and any URL that goes to the targeted 28 * directory (or its sub-directories) should "follow" those restrictions. 29 * 30 * On case-sensitive volumes, a URI must 31 * always match the actual path, in order for the file to be fetched. Any 32 * <Directory> statement will consequently be enforced. Because if there 33 * is a case-mismatch a file-not-found error will be returned and if 34 * there is no case-mismatch then relevant <Directory> statements will 35 * be walked through while parsing the URI. 36 * 37 * On case-insensitive HFS volumes, a URI may 38 * not always case-match the actual path to the file that needs to be 39 * fetched. That means that <Directory> statements may not be walked 40 * through if a case-mismatch appears in the URI (or in the statement) 41 * in regards to the actual path stored on disk. Consequently, some 42 * restrictive statements may be missed but the target file may still be 43 * returned as response. In this situation we have a problem: to solve 44 * it we should refuse such URL that case-mismatches part of the path 45 * which, if not miscased, would actually make a <Directory> statement 46 * currently configured applies. 47 * 48 * That is what this module does. Consequently, when this module is 49 * installed, some "pseudo-case-sensitivity" is enforced when Apache 50 * deals with case-insensitive HFS volumes. 51 * 52 * 13-JUN-2001 [JFA, Apple Computer, Inc.] 53 * Initial version for Mac OS X Server 10.0. 54 */ 55 56 57#define CORE_PRIVATE 58#include "apr.h" 59#include "apr_strings.h" 60#include "httpd.h" 61#include "http_config.h" 62#include "http_core.h" 63#include "http_request.h" 64#include "http_protocol.h" 65#include "http_log.h" 66#include "http_main.h" 67#include "util_script.h" 68#include <ctype.h> 69 70#define __MACHINEEXCEPTIONS__ 71#define __DRIVERSERVICES__ 72#include <CoreServices/../Frameworks/CarbonCore.framework/Headers/MacErrors.h> 73#include <CoreFoundation/CFString.h> 74 75#include <unistd.h> 76 77 78module AP_MODULE_DECLARE_DATA hfs_apple_module; 79 80 81/* 82 * Our core data structure: each entry in the table is composed 83 * of a key (the path of a <Directory> statement, no matter what 84 * server it applies to) and a value that tells whether its 85 * volume is HFS or not (case-sensitive=0 or 1). Unfortunately 86 * the work required to fill this table will be repeated for 87 * each Apache child process (but there is nothing new here!) 88 */ 89static apr_pool_t *g_pool = NULL; 90static apr_array_header_t *directories = NULL; 91 92typedef struct dir_rec { 93 char *dir_path; 94 int case_sens; 95} dir_rec; 96 97/* 98 * Support routine that populates our table of directories 99 * to be considered. We ignore what server configuration is 100 * attached to the directory because it does not matter. 101 */ 102static void add_directory_entry(request_rec *r, char *path) { 103 char *dir_path; 104 int i,case_sens = 0; 105 dir_rec **elt; 106 size_t len = strlen(path) + 2; 107 108 /* malloc dir_path so we can explicitly free it if the path 109 * already exists in the cache, rather than leaving it in 110 * apache's main pool. 111 */ 112 dir_path = malloc(len); 113 if( dir_path == NULL ) return; 114 strlcpy(dir_path, path, len); 115 116 /* Make sure input path has a trailing slash */ 117 if (path[strlen(path) - 1] != '/') 118 strlcat(dir_path, "/", len); 119 120 /* If the entry already exists then get out */ 121 for (i = 0; i < directories->nelts; i++) { 122 dir_rec *entry = ((dir_rec**) directories->elts)[i]; 123 if (strcmp(dir_path, entry->dir_path) == 0) { 124 free(dir_path); 125 return; 126 } 127 } 128 129 /* Figure whether the targeted volume is case-sensitive */ 130 case_sens = pathconf(path, _PC_CASE_SENSITIVE); 131 //Non-existent paths may be considered case-sensitive 132 133 /* Add new entry to the table (ignore errors) */ 134 elt = apr_array_push(directories); 135 *elt = (dir_rec*) apr_palloc(g_pool, sizeof(dir_rec)); 136 if (*elt == NULL) return; 137 /* Duplicate the path into apache's main pool (along with the rest 138 * of the structure) so everything stays together. Then free what 139 * we've malloc'd. To do: Consider normalizing dir_path here. 140 */ 141 (*elt)->dir_path = apr_pstrdup(g_pool, dir_path); 142 free(dir_path); 143 (*elt)->case_sens = case_sens; 144 145 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 146 "mod_hfs_apple: %s is %s", 147 (*elt)->dir_path, (*elt)->case_sens ? "case-sensitive" : "case-insensitive"); 148} 149 150/* 151 * Support routine that updates our table of directory entries, 152 * should be called whenever a request is received. 153 */ 154static void update_directory_entries(request_rec *r) { 155 core_server_config *sconf = (core_server_config*) 156 ap_get_module_config(r->server->module_config, &core_module); 157 void **sec = (void**) sconf->sec_dir->elts; 158 int i,num_sec = sconf->sec_dir->nelts; 159 160 /* Parse all "<Directory>" statements for 'r->server' */ 161 for (i = 0; i < num_sec; ++i) { 162 core_dir_config *entry_core = (core_dir_config*) 163 ap_get_module_config(sec[i], &core_module); 164 if (entry_core == NULL || entry_core->d == NULL) continue; 165 add_directory_entry(r, entry_core->d); 166 } 167} 168 169/* 170 * Determine whether child path refers to a subdirectory of parent path, with equivalance determined by 171 * comparing their file system representation. Only called for case-insensitive parents, with non-ascii 172 * characters in the argument strings, since the other cases are handled by compare_paths. 173 */ 174static int compare_non_ascii_paths(const char *parent, const char *child, int *related, int *deny, request_rec* r) { 175 CFStringRef parentRef = CFStringCreateWithCString(NULL, parent, kCFStringEncodingUTF8); 176 if (!parentRef) { 177 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 178 "mod_hfs_apple: Cannot encode parent %s. Skipping.", parent); 179 return 0; 180 } 181 182 CFStringRef childRef = CFStringCreateWithCString(NULL, child, kCFStringEncodingUTF8); 183 if (!childRef) { 184 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 185 "mod_hfs_apple: Cannot encode child %s. Denying access.", child); 186 *deny = 1; 187 return 0; 188 } 189 190 int parentStrLen = strlen(parent); 191 int parentLength = CFStringGetLength(parentRef); 192 int childLength = CFStringGetLength(childRef); 193 if (CFStringHasSuffix(parentRef, CFSTR("/"))) { 194 CFRelease(parentRef); 195 parentRef = CFStringCreateWithSubstring(NULL, parentRef, CFRangeMake(0, --parentLength)); 196 parentStrLen--; 197 } 198 if (CFStringHasSuffix(childRef, CFSTR("/"))) { 199 CFRelease(childRef); 200 childRef = CFStringCreateWithSubstring(NULL, childRef, CFRangeMake(0, --childLength)); 201 } 202 char fsrChild[PATH_MAX]; 203 if (!CFStringGetFileSystemRepresentation(childRef, fsrChild, sizeof(fsrChild))) { 204 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 205 "mod_hfs_apple: Cannot get file system representation for child %s. Denying access.", child); 206 CFRelease(childRef); 207 *deny = 1; 208 return 0; 209 } 210 CFRelease(childRef); 211 212 char fsrParent[PATH_MAX]; 213 if (!CFStringGetFileSystemRepresentation(parentRef, fsrParent, sizeof(fsrParent))) { 214 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 215 "mod_hfs_apple: Cannot get file system representation for parent %s. Skipping.", parent); 216 CFRelease(parentRef); 217 return 0; 218 } 219 CFRelease(parentRef); 220 221 size_t fsrLen = strlen(fsrParent); 222 if (!strncasecmp(fsrParent, fsrChild, fsrLen)) { 223 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 224 "mod_hfs_apple: Comparing FSR: %s == %s, len = %ld", fsrParent, fsrChild, fsrLen); 225 *related = 1; 226 return parentStrLen; 227 } else { 228 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 229 "mod_hfs_apple: Comparing FSR: %s != %s, len = %ld", fsrParent, fsrChild, fsrLen); 230 *related = 0; 231 return 0; 232 } 233} 234 235 236/* 237 * Support routine that does a string compare of two paths (do not 238 * care if trailing slashes are present). Return the number of 239 * characters matched (or 0 else) if both paths are equal or if 240 * 'child' is a sub-directory of 'parent'. In that very case also 241 * returns 'related'=1. 242 */ 243static int compare_paths(const char *parent, const char *child, 244 int *related, int *deny, request_rec* r) { 245 size_t pl,cl,i; 246 const char *p,*c; 247 size_t n = 0; 248 249 *related = 0; 250 251 /* Strip out trailing slashes */ 252 pl = (size_t) strlen(parent); 253 if (pl == 0) return 0; 254 if (parent[pl - 1] == '/') pl--; 255 cl = (size_t) strlen(child); 256 if (cl == 0) return 0; 257 if (child[cl - 1] == '/') cl--; 258 if (cl < pl) return 0; 259 /* Compare both paths */ 260 for (p = parent,c = child,i = pl; i > 0; i--) { 261 if (!isascii(*p) || !isascii(*c)) 262 return (compare_non_ascii_paths(parent, child, related, deny, r)); 263 if (tolower(*p++) != tolower(*c++)) break; 264 n++; 265 } 266 if (i > 0 || (cl > pl && *c != '/')) return 0; 267 *related = cl >= pl; 268 return n; 269} 270 271/* Return 1 if string contains ignorable Unicode sequence. 272 * From 12830770: 273 * (\xFC[\x80-\x83])|(\xF8[\x80-\x87])|(\xF0[\x80-\x8F])|(\xEF\xBB\xBF)|(\xE2\x81[\xAA-\xAF])|(\xE2\x80[\x8C-\x8F\xAA-\xAE]) 274 */ 275static int contains_ignorable_sequence(unsigned char* s, __attribute__((unused)) request_rec* r) { 276 size_t len = strlen((char*)s); 277 if (len <= 2) return 0; 278 size_t i; 279 for (i = 0; i <= len - 2; i++) { 280 // 2-char sequences 281 if (s[i] == (unsigned char)'\xFC' && (unsigned char)'\x80' <= s[i+1] && s[i+1] <= (unsigned char)'\x83') return 1; 282 if (s[i] == (unsigned char)'\xF8' && (unsigned char)'\x80' <= s[i+1] && s[i+1] <= (unsigned char)'\x87') return 1; 283 if (s[i] == (unsigned char)'\xF0' && (unsigned char)'\x80' <= s[i+1] && s[i+1] <= (unsigned char)'\x8F') return 1; 284 if (i <= len - 3) { 285 // 3-char sequences 286 //ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 287 // "mod_hfs_apple: 3-char %x %x %x", s[i], s[i+1], s[i+2]); 288 289 if (s[i] == (unsigned char)'\xEF' && s[i+1] == (unsigned char)'\xBB' && s[i+2] == (unsigned char)'\xBF') return 1; 290 if (s[i] == (unsigned char)'\xE2' && s[i+1] == (unsigned char)'\x81' && (unsigned char)'\xAA' <= s[i+2] && s[i+2] <= (unsigned char)'\xAF') return 1; 291 if (s[i] == (unsigned char)'\xE2' && s[i+1] == (unsigned char)'\x80' && (((unsigned char)'\x8C' <= s[i+2] && s[i+2] <= (unsigned char)'\x8F') || ((unsigned char)'\xAA' <= s[i+2] && s[i+2] <= (unsigned char)'\xAE'))) return 1; 292 } 293 } 294 return 0; 295} 296 297 298#pragma mark- 299/* 300 * Pre-run fixups: refuse a URL that is mis-cased if it happens 301 * there is at least one <Directory> statement that should have 302 * applied. As input, this routine is passed a valid 'filename' 303 * that can be a path to a directory or to a file. 304 */ 305static int hfs_apple_module_fixups(request_rec *r) { 306 int i,found; 307 size_t max_n_matches; 308 char *url_path; 309 size_t len; 310 311/* 312 * Forbid access to URIs with ignorable Unicode character sequences 313*/ 314 if (contains_ignorable_sequence((unsigned char*)r->filename, r)) { 315 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, 316 "mod_hfs_apple: URI %s has ignorable character sequence. Denying access.", 317 r->filename); 318 return HTTP_FORBIDDEN; 319 } 320 321 /* First update table of directory entries if necessary */ 322 update_directory_entries(r); 323 324 /* 325 * Then compare our path to each <Directory> statement we 326 * found (case-insensitive compare) in order to find which 327 * one applies, example (the second one would apply here): 328 * 'filename'= 329 * /Library/WebServer/Documents/MyFolder/printenv.cgi 330 * 'directories' table= 331 * /Library/WebServer/Documents/ 332 * /Library/WebServer/Documents/MyFolder/ 333 * /Library/WebServer/Documents/MyFolder/Zero/ 334 * /Library/WebServer/Documents/MyFolder/Zero/One/ 335 */ 336 max_n_matches = 0; 337 found = -1; 338 len = strlen(r->filename); 339 int deny = 0; 340 if (r->filename[len - 1] != '/') { 341 url_path = malloc(len + 2); 342 if( url_path == NULL ) return HTTP_FORBIDDEN; 343 strlcpy(url_path, r->filename, len + 2); 344 strlcat(url_path, "/", len + 2); 345 } else { 346 url_path = malloc(len + 1); 347 if( url_path == NULL ) return HTTP_FORBIDDEN; 348 strlcpy(url_path, r->filename, len + 1); 349 } 350 for (i = 0; i < directories->nelts; i++) { 351 int related; 352 size_t n_matches; 353 dir_rec *entry = ((dir_rec**) directories->elts)[i]; 354 if (entry->case_sens == 1) continue; 355 n_matches = compare_paths( 356 entry->dir_path, url_path, &related, &deny, r); 357 if (deny) { 358 free(url_path); 359 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, 360 "mod_hfs_apple: Cannot encode for comparison, %s vs %s; denying access.", entry->dir_path, url_path); 361 return HTTP_FORBIDDEN; 362 } 363 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 364 "mod_hfs_apple: compare_paths %s vs %s, related=%d", entry->dir_path, url_path, related); 365 if (n_matches > 0 366 && n_matches > max_n_matches && related == 1) { 367 max_n_matches = n_matches; 368 found = i; 369 } 370 } 371 if (found < 0) { 372 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 373 "mod_hfs_apple: Allowing access with no matching directory. filename = %s", r->filename); 374 free(url_path); 375 return OK; 376 } 377 378 /* 379 * We found at least one <Directory> statement that defines 380 * the most immediate parent of 'filename'. Do a regular 381 * case-sensitive compare on the directory portion of it. If 382 * not-equal then return an error. 383 */ 384 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 385 "mod_hfs_apple: Final check compares: %s vs %s, length %ld", 386 r->filename, ((dir_rec**) directories->elts)[found]->dir_path, max_n_matches); 387 388 if (strncmp(((dir_rec**) directories->elts)[found]->dir_path, 389 url_path, max_n_matches) != 0) { 390 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, 391 "mod_hfs_apple: Mis-cased URI or unacceptable Unicode in URI: %s, wants: %s", 392 r->filename, 393 ((dir_rec**) directories->elts)[found]->dir_path); 394 free(url_path); 395 return HTTP_FORBIDDEN; 396 } 397 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r, 398 "mod_hfs_apple: Allowing access with matching directory. filename = %s", r->filename); 399 free(url_path); 400 return OK; 401} 402 403/* 404 * Initialization (called only once by Apache parent process). 405 * We will be using the main pool not the request's one! 406 */ 407static void hfs_apple_module_init(apr_pool_t *p, __attribute__((unused)) server_rec *s ) { 408 g_pool = p; 409 directories = apr_array_make(g_pool, 4, sizeof(dir_rec*)); 410} 411 412 413static void register_hooks(__attribute__((unused)) apr_pool_t *p) 414{ 415 ap_hook_child_init(hfs_apple_module_init, NULL, NULL, APR_HOOK_MIDDLE); 416 ap_hook_fixups(hfs_apple_module_fixups, NULL, NULL, APR_HOOK_MIDDLE); 417} 418 419 420#pragma mark DispatchTable 421/* 422 * Module dispatch table. 423 */ 424module AP_MODULE_DECLARE_DATA hfs_apple_module = { 425 STANDARD20_MODULE_STUFF, 426 NULL, /* dir config creater */ 427 NULL, /* dir merger --- default is to override */ 428 NULL, /* server config */ 429 NULL, /* merge server config */ 430 NULL, /* command apr_table_t */ 431 register_hooks /* register hooks */ 432}; 433