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