1 /*
2  * part of owftpd By Paul H Alfille
3  * The whole is GPLv2 licenced though the ftp code was more liberally licenced when first used.
4  */
5 
6 #include "owftpd.h"
7 #include <limits.h>
8 #include <stdarg.h>
9 #include <fnmatch.h>
10 
11 /* AIX requires this to be the first thing in the file.  */
12 #ifndef __GNUC__
13 # if HAVE_ALLOCA_H
14 #  include <alloca.h>
15 # else
16 #  ifdef _AIX
17 #pragma alloca
18 #  else
19 #   ifndef alloca				/* predefined by HP cc +Olibcalls */
20 char *alloca();
21 #   endif
22 #  endif
23 # endif
24 #endif
25 
26 static void fdprintf(FILE_DESCRIPTOR_OR_ERROR file_descriptor, const char *fmt, ...);
27 static void List_show(struct file_parse_s *fps, const struct parsedname *pn);
28 static void WildLexParse(struct file_parse_s *fps, ASCII * match);
29 static char *skip_ls_options(char *filespec);
30 
31 /* if no localtime_r() is available, provide one */
32 #ifndef HAVE_LOCALTIME_R
33 #include <pthread.h>
34 
localtime_r(const time_t * timep,struct tm * timeptr)35 struct tm *localtime_r(const time_t * timep, struct tm *timeptr)
36 {
37 	static pthread_mutex_t time_lock = PTHREAD_MUTEX_INITIALIZER;
38 
39 	_MUTEX_LOCK(time_lock);
40 	*timeptr = *(localtime(timep));
41 	_MUTEX_UNLOCK(time_lock);
42 	return timeptr;
43 }
44 #endif							/* HAVE_LOCALTIME_R */
45 
List_show(struct file_parse_s * fps,const struct parsedname * pn)46 static void List_show(struct file_parse_s *fps, const struct parsedname *pn)
47 {
48 	struct stat stbuf;
49 	time_t now;
50 	struct tm tm_now;
51 	double age;
52 	char date_buf[13];
53 	ASCII *perms[] = {
54 		"---------",
55 		"--x--x--x",
56 		"-w--w--w-",
57 		"-wx-wx-wx",
58 		"r--r--r--",
59 		"r-xr-xr-x",
60 		"rw-rw-rw-",
61 		"rwxrwxrwx",
62 	};
63 	LEVEL_DEBUG("List_show %s", pn->path);
64 	switch (fps->fle) {
65 		case file_list_list:
66 			FS_fstat_postparse(&stbuf, pn);
67 			fdprintf(fps->out, "%s%s", stbuf.st_mode & S_IFDIR ? "d" : "-", perms[stbuf.st_mode & 0x07]);
68 			/* fps->output link & ownership information */
69 			fdprintf(fps->out, " %3d %-8d %-8d %8lu ", stbuf.st_nlink, stbuf.st_uid, stbuf.st_gid, (unsigned long) stbuf.st_size);
70 			/* fps->output date */
71 			time(&now);
72 			localtime_r(&stbuf.st_mtime, &tm_now);
73 			age = difftime(now, stbuf.st_mtime);
74 			if ((age > 60 * 60 * 24 * 30 * 6)
75 				|| (age < -(60 * 60 * 24 * 30 * 6))) {
76 				strftime(date_buf, sizeof(date_buf), "%b %e  %Y", &tm_now);
77 			} else {
78 				strftime(date_buf, sizeof(date_buf), "%b %e %H:%M", &tm_now);
79 			}
80 			fdprintf(fps->out, "%s ", date_buf);
81 			/* Fall Through */
82 		case file_list_nlst:
83 			/* fps->output filename */
84 			fdprintf(fps->out, "%s\r\n", &pn->path[fps->start]);
85 	}
86 }
87 
FileLexParse(struct file_parse_s * fps)88 void FileLexParse(struct file_parse_s *fps)
89 {
90 	struct parsedname pn;
91 	while (1) {
92 		switch (fps->pse) {
93 		case parse_status_init:
94 			LEVEL_DEBUG("FTP parse_status_init Path<%s> File <%s>", fps->buffer, fps->rest);
95 			/* fps->buffer is absolute */
96 			/* trailing / only at root */
97 			fps->ret = 0;
98 			fps->start = strlen(fps->buffer);
99 			if (fps->start > 1) {
100 				++fps->start;
101 			}
102 			fps->rest = skip_ls_options(fps->rest);
103 			if (fps->rest == NULL || fps->rest[0] == '\0') {
104 				fps->pse = parse_status_tame;
105 			} else {
106 				if (fps->rest[0] == '/') {	// root specification
107 					fps->buffer[1] = '\0';
108 					fps->start = 0;
109 					++fps->rest;
110 				}
111 				fps->pse = parse_status_init2;
112 			}
113 			break;
114 		case parse_status_init2:
115 			LEVEL_DEBUG("FTP parse_status_init2 Path<%s> File <%s>", fps->buffer, fps->rest);
116 			/* fps->buffer is absolute */
117 			/* trailing / only at root */
118 			if ((fps->rest[0] == '.' && fps->rest[1] == '.')
119 				|| strpbrk(fps->rest, "*[?")) {
120 				fps->pse = parse_status_back;
121 			} else {
122 				fps->pse = parse_status_tame;
123 			}
124 			break;
125 		case parse_status_back:
126 			LEVEL_DEBUG("FTP parse_status_back Path<%s> File <%s>", fps->buffer, fps->rest);
127 			/* fps->buffer is absolute */
128 			/* trailing / only at root */
129 			if (fps->rest[0] == '.' && fps->rest[1] == '.') {
130 				// Move back
131 				ASCII *back = strrchr(fps->buffer, '/');
132 				back[1] = '\0';
133 				fps->start = strlen(fps->buffer);
134 				// look for next file part
135 				if (fps->rest[2] == '\0') {
136 					fps->pse = parse_status_last;
137 					fps->rest = NULL;
138 				} else if (fps->rest[2] == '/') {
139 					fps->pse = parse_status_next;
140 					fps->rest = &fps->rest[3];
141 				} else {
142 					fps->ret = -ENOENT;
143 					return;
144 				}
145 			} else {
146 				fps->pse = parse_status_next;	// off the double dot trail
147 			}
148 			break;
149 		case parse_status_next:
150 			LEVEL_DEBUG("FTP parse_status_next Path<%s> File <%s>", fps->buffer, fps->rest);
151 			/* fps->buffer is absolute */
152 			/* trailing / only at root */
153 			if (fps->rest == NULL || fps->rest[0] == '\0') {
154 				fps->pse = parse_status_last;
155 			} else {
156 				ASCII *oldrest = strsep(&fps->rest, "/");
157 				if (strpbrk(oldrest, "*[?")) {
158 					WildLexParse(fps, oldrest);
159 					return;
160 				} else {
161 					if (strlen(fps->buffer) + strlen(oldrest) + 4 > PATH_MAX) {
162 						fps->ret = -ENAMETOOLONG;
163 						return;
164 					}
165 					if (fps->buffer[1]) {
166 						strcat(fps->buffer, "/");
167 					}
168 					strcat(fps->buffer, oldrest);
169 					fps->pse = parse_status_next;
170 				}
171 			}
172 			break;
173 		case parse_status_tame:
174 			LEVEL_DEBUG("FTP parse_status_tame Path<%s> File <%s>", fps->buffer, fps->rest);
175 			/* fps->buffer is absolute */
176 			/* trailing / only at root */
177 			if ( fps-> rest != NULL ) {
178 				if ( strlen(fps->buffer) + strlen(fps->rest) + 4 > PATH_MAX ) {
179 					fps->ret = -ENAMETOOLONG;
180 					return;
181 				}
182 				if ( fps->buffer[1] != 0 ) {
183 					strcat(fps->buffer, "/");
184 				}
185 				strcat(fps->buffer, fps->rest);
186 			}
187 			if (FS_ParsedName(fps->buffer, &pn) == 0) {
188 				if (IsDir(&pn)) {
189 					fps->start = strlen(fps->buffer) + 1;
190 					if (fps->start == 2) {
191 						fps->start = 1;
192 					} else if (fps->buffer[fps->start - 2] == '/') {
193 						--fps->start;
194 						fps->buffer[fps->start - 1] = '\0';
195 					}
196 					fps->rest = NULL;
197 					WildLexParse(fps, "*");
198 				} else {
199 					List_show(fps, &pn);
200 				}
201 				FS_ParsedName_destroy(&pn);
202 			} else {
203 				fps->ret = -ENOENT;
204 			}
205 			return;
206 		case parse_status_last:
207 			LEVEL_DEBUG("FTP parse_status_last Path<%s> File <%s>", fps->buffer, fps->rest);
208 			/* fps->buffer is absolute */
209 			/* trailing / only at root */
210 			if (fps->rest && (strlen(fps->buffer) + strlen(fps->rest) + 4 > PATH_MAX)) {
211 				fps->ret = -ENAMETOOLONG;
212 				return;
213 			}
214 			if (FS_ParsedNamePlus(fps->buffer, fps->rest, &pn) == 0) {
215 				List_show(fps, &pn);
216 				FS_ParsedName_destroy(&pn);
217 			}
218 			return;
219 		}
220 	}
221 }
222 
223 struct wildlexparse {
224 	ASCII *end;
225 	ASCII *match;
226 	struct file_parse_s *fps;
227 };
228 /* Called for each directory element, and operates recursively */
229 /* uses C library fnmatch for file wildcard comparisons */
WildLexParseCallback(void * v,const struct parsedname * const pn_entry)230 static void WildLexParseCallback(void *v, const struct parsedname *const pn_entry)
231 {
232 	struct wildlexparse *wlp = v;
233 	struct file_parse_s fps;	// duplicate for recursive call
234 
235 	/* get real name from parsedname struct */
236 	strcpy(&wlp->end[1], FS_DirName(pn_entry));
237 
238 	LEVEL_DEBUG("WildLexParseCallback: Try %s vs %s", &wlp->end[1], wlp->match);
239 	if (fnmatch(wlp->match, &wlp->end[1], FNM_PATHNAME)) {
240 		return;
241 	}
242 
243 	/* Matched! So set up for recursive call on nect elements in path name */
244 	memcpy(&fps, wlp->fps, sizeof(fps));
245 	fps.pse = parse_status_next;
246 	FileLexParse(&fps);
247 }
248 
249 /* Called for each directory, calls WildLEexParseCallback on each element
250    to see if matches wildcards (even used for normal listings with * wildcard)
251  */
WildLexParse(struct file_parse_s * fps,ASCII * match)252 static void WildLexParse(struct file_parse_s *fps, ASCII * match)
253 {
254 	struct parsedname pn;
255 	/* Embedded callback function */
256 
257 	LEVEL_DEBUG("FTP Wildcard pattern matching: Path=%s, Pattern=%s, File=%s", SAFESTRING(fps->buffer), SAFESTRING(match), SAFESTRING(fps->rest));
258 
259 	/* Check potential length */
260 	if (strlen(fps->buffer) + OW_FULLNAME_MAX + 2 > PATH_MAX) {
261 		fps->ret = -ENAMETOOLONG;
262 		return;
263 	}
264 
265 	/* Can we understand the current path? */
266 	if (FS_ParsedName(fps->buffer, &pn) != 0 ) {
267 		fps->ret = -ENOENT;
268 		return;
269 	}
270 
271 	if (pn.selected_filetype) {
272 		fps->ret = -ENOTDIR;
273 	} else {
274 		struct wildlexparse wlp = { NULL, match, fps, };
275 		int root = (fps->buffer[1] == '\0');
276 
277 		wlp.end = &fps->buffer[strlen(fps->buffer)];
278 
279 		if (root) {
280 			--wlp.end;
281 		}
282 		wlp.end[0] = '/';
283 		FS_dir(WildLexParseCallback, &wlp, &pn);
284 		if (root) {
285 			++wlp.end;
286 		}
287 		wlp.end[0] = '\0';		// restore fps->buffer
288 	}
289 	FS_ParsedName_destroy(&pn);
290 
291 }
292 
293 /* write with care for max length and incomplete outout */
fdprintf(FILE_DESCRIPTOR_OR_ERROR file_descriptor,const char * fmt,...)294 static void fdprintf(FILE_DESCRIPTOR_OR_ERROR file_descriptor, const char *fmt, ...)
295 {
296 	char buf[PATH_MAX + 1];
297 	ssize_t buflen;
298 	va_list ap;
299 	int amt_written;
300 	int write_ret;
301 
302 	daemon_assert(FILE_DESCRIPTOR_VALID(file_descriptor));
303 	daemon_assert(fmt != NULL);
304 
305 	va_start(ap, fmt);
306 	buflen = vsnprintf(buf, sizeof(buf), fmt, ap);
307 	va_end(ap);
308 	if (buflen <= 0) {
309 		return;
310 	}
311 	if ((size_t) buflen >= sizeof(buf)) {
312 		buflen = sizeof(buf) - 1;
313 	}
314 
315 	amt_written = 0;
316 	while (amt_written < buflen) {
317 		write_ret = write(file_descriptor, buf + amt_written, buflen - amt_written);
318 		if (write_ret <= 0) {
319 			return;
320 		}
321 		amt_written += write_ret;
322 	}
323 }
324 
325 /*
326   hack workaround clients like Midnight Commander that send:
327       LIST -al /dirname
328 */
skip_ls_options(char * filespec)329 static char *skip_ls_options(char *filespec)
330 {
331 	daemon_assert(filespec != NULL);
332 
333 	for (;;) {
334 		/* end when we've passed all options */
335 		if (*filespec != '-') {
336 			break;
337 		}
338 		filespec++;
339 
340 		/* if we find "--", eat it and any following whitespace then return */
341 		if ((filespec[0] == '-') && (filespec[1] == ' ')) {
342 			filespec += 2;
343 			while (isspace(*filespec)) {
344 				filespec++;
345 			}
346 			break;
347 		}
348 
349 		/* otherwise, skip this option */
350 		while ((*filespec != '\0') && !isspace(*filespec)) {
351 			filespec++;
352 		}
353 
354 		/* and skip any whitespace */
355 		while (isspace(*filespec)) {
356 			filespec++;
357 		}
358 	}
359 
360 	daemon_assert(filespec != NULL);
361 
362 	return filespec;
363 }
364