1 | /* |
2 | +----------------------------------------------------------------------+ |
3 | | PHP Version 5 | |
4 | +----------------------------------------------------------------------+ |
5 | | Copyright (c) 1997-2015 The PHP Group | |
6 | +----------------------------------------------------------------------+ |
7 | | This source file is subject to version 3.01 of the PHP license, | |
8 | | that is bundled with this package in the file LICENSE, and is | |
9 | | available through the world-wide-web at the following url: | |
10 | | http://www.php.net/license/3_01.txt | |
11 | | If you did not receive a copy of the PHP license and are unable to | |
12 | | obtain it through the world-wide-web, please send a note to | |
13 | | license@php.net so we can mail you a copy immediately. | |
14 | +----------------------------------------------------------------------+ |
15 | | Author: Sascha Schumann <sascha@schumann.cx> | |
16 | +----------------------------------------------------------------------+ |
17 | */ |
18 | |
19 | /* $Id$ */ |
20 | |
21 | #include "php.h" |
22 | |
23 | #include <sys/stat.h> |
24 | #include <sys/types.h> |
25 | |
26 | #if HAVE_SYS_FILE_H |
27 | #include <sys/file.h> |
28 | #endif |
29 | |
30 | #if HAVE_DIRENT_H |
31 | #include <dirent.h> |
32 | #endif |
33 | |
34 | #ifdef PHP_WIN32 |
35 | #include "win32/readdir.h" |
36 | #endif |
37 | #include <time.h> |
38 | |
39 | #include <fcntl.h> |
40 | #include <errno.h> |
41 | |
42 | #if HAVE_UNISTD_H |
43 | #include <unistd.h> |
44 | #endif |
45 | |
46 | #include "php_session.h" |
47 | #include "mod_files.h" |
48 | #include "ext/standard/flock_compat.h" |
49 | #include "php_open_temporary_file.h" |
50 | |
51 | #define FILE_PREFIX "sess_" |
52 | |
53 | #ifdef PHP_WIN32 |
54 | # ifndef O_NOFOLLOW |
55 | # define O_NOFOLLOW 0 |
56 | # endif |
57 | #endif |
58 | |
59 | typedef struct { |
60 | int fd; |
61 | char *lastkey; |
62 | char *basedir; |
63 | size_t basedir_len; |
64 | size_t dirdepth; |
65 | size_t st_size; |
66 | int filemode; |
67 | } ps_files; |
68 | |
69 | ps_module ps_mod_files = { |
70 | PS_MOD_SID(files) |
71 | }; |
72 | |
73 | |
74 | static char *ps_files_path_create(char *buf, size_t buflen, ps_files *data, const char *key) |
75 | { |
76 | size_t key_len; |
77 | const char *p; |
78 | int i; |
79 | int n; |
80 | |
81 | key_len = strlen(key); |
82 | if (key_len <= data->dirdepth || |
83 | buflen < (strlen(data->basedir) + 2 * data->dirdepth + key_len + 5 + sizeof(FILE_PREFIX))) { |
84 | return NULL; |
85 | } |
86 | |
87 | p = key; |
88 | memcpy(buf, data->basedir, data->basedir_len); |
89 | n = data->basedir_len; |
90 | buf[n++] = PHP_DIR_SEPARATOR; |
91 | for (i = 0; i < (int)data->dirdepth; i++) { |
92 | buf[n++] = *p++; |
93 | buf[n++] = PHP_DIR_SEPARATOR; |
94 | } |
95 | memcpy(buf + n, FILE_PREFIX, sizeof(FILE_PREFIX) - 1); |
96 | n += sizeof(FILE_PREFIX) - 1; |
97 | memcpy(buf + n, key, key_len); |
98 | n += key_len; |
99 | buf[n] = '\0'; |
100 | |
101 | return buf; |
102 | } |
103 | |
104 | #ifndef O_BINARY |
105 | # define O_BINARY 0 |
106 | #endif |
107 | |
108 | static void ps_files_close(ps_files *data) |
109 | { |
110 | if (data->fd != -1) { |
111 | #ifdef PHP_WIN32 |
112 | /* On Win32 locked files that are closed without being explicitly unlocked |
113 | will be unlocked only when "system resources become available". */ |
114 | flock(data->fd, LOCK_UN); |
115 | #endif |
116 | close(data->fd); |
117 | data->fd = -1; |
118 | } |
119 | } |
120 | |
121 | static void ps_files_open(ps_files *data, const char *key TSRMLS_DC) |
122 | { |
123 | char buf[MAXPATHLEN]; |
124 | struct stat sbuf; |
125 | int ret; |
126 | |
127 | if (data->fd < 0 || !data->lastkey || strcmp(key, data->lastkey)) { |
128 | if (data->lastkey) { |
129 | efree(data->lastkey); |
130 | data->lastkey = NULL; |
131 | } |
132 | |
133 | ps_files_close(data); |
134 | |
135 | if (php_session_valid_key(key) == FAILURE) { |
136 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "The session id is too long or contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,'" ); |
137 | return; |
138 | } |
139 | |
140 | if (!ps_files_path_create(buf, sizeof(buf), data, key)) { |
141 | return; |
142 | } |
143 | |
144 | data->lastkey = estrdup(key); |
145 | |
146 | /* O_NOFOLLOW to prevent us from following evil symlinks */ |
147 | #ifdef O_NOFOLLOW |
148 | data->fd = VCWD_OPEN_MODE(buf, O_CREAT | O_RDWR | O_BINARY | O_NOFOLLOW, data->filemode); |
149 | #else |
150 | /* Check to make sure that the opened file is not outside of allowable dirs. |
151 | This is not 100% safe but it's hard to do something better without O_NOFOLLOW */ |
152 | if(PG(open_basedir) && lstat(buf, &sbuf) == 0 && S_ISLNK(sbuf.st_mode) && php_check_open_basedir(buf TSRMLS_CC)) { |
153 | return; |
154 | } |
155 | data->fd = VCWD_OPEN_MODE(buf, O_CREAT | O_RDWR | O_BINARY, data->filemode); |
156 | #endif |
157 | |
158 | if (data->fd != -1) { |
159 | #ifndef PHP_WIN32 |
160 | /* check that this session file was created by us or root – we |
161 | don't want to end up accepting the sessions of another webapp */ |
162 | if (fstat(data->fd, &sbuf) || (sbuf.st_uid != 0 && sbuf.st_uid != getuid() && sbuf.st_uid != geteuid())) { |
163 | close(data->fd); |
164 | data->fd = -1; |
165 | return; |
166 | } |
167 | #endif |
168 | do { |
169 | ret = flock(data->fd, LOCK_EX); |
170 | } while (ret == -1 && errno == EINTR); |
171 | |
172 | #ifdef F_SETFD |
173 | # ifndef FD_CLOEXEC |
174 | # define FD_CLOEXEC 1 |
175 | # endif |
176 | if (fcntl(data->fd, F_SETFD, FD_CLOEXEC)) { |
177 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "fcntl(%d, F_SETFD, FD_CLOEXEC) failed: %s (%d)" , data->fd, strerror(errno), errno); |
178 | } |
179 | #endif |
180 | } else { |
181 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "open(%s, O_RDWR) failed: %s (%d)" , buf, strerror(errno), errno); |
182 | } |
183 | } |
184 | } |
185 | |
186 | static int ps_files_cleanup_dir(const char *dirname, int maxlifetime TSRMLS_DC) |
187 | { |
188 | DIR *dir; |
189 | char dentry[sizeof(struct dirent) + MAXPATHLEN]; |
190 | struct dirent *entry = (struct dirent *) &dentry; |
191 | struct stat sbuf; |
192 | char buf[MAXPATHLEN]; |
193 | time_t now; |
194 | int nrdels = 0; |
195 | size_t dirname_len; |
196 | |
197 | dir = opendir(dirname); |
198 | if (!dir) { |
199 | php_error_docref(NULL TSRMLS_CC, E_NOTICE, "ps_files_cleanup_dir: opendir(%s) failed: %s (%d)" , dirname, strerror(errno), errno); |
200 | return (0); |
201 | } |
202 | |
203 | time(&now); |
204 | |
205 | dirname_len = strlen(dirname); |
206 | |
207 | /* Prepare buffer (dirname never changes) */ |
208 | memcpy(buf, dirname, dirname_len); |
209 | buf[dirname_len] = PHP_DIR_SEPARATOR; |
210 | |
211 | while (php_readdir_r(dir, (struct dirent *) dentry, &entry) == 0 && entry) { |
212 | /* does the file start with our prefix? */ |
213 | if (!strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1)) { |
214 | size_t entry_len = strlen(entry->d_name); |
215 | |
216 | /* does it fit into our buffer? */ |
217 | if (entry_len + dirname_len + 2 < MAXPATHLEN) { |
218 | /* create the full path.. */ |
219 | memcpy(buf + dirname_len + 1, entry->d_name, entry_len); |
220 | |
221 | /* NUL terminate it and */ |
222 | buf[dirname_len + entry_len + 1] = '\0'; |
223 | |
224 | /* check whether its last access was more than maxlifetime ago */ |
225 | if (VCWD_STAT(buf, &sbuf) == 0 && |
226 | (now - sbuf.st_mtime) > maxlifetime) { |
227 | VCWD_UNLINK(buf); |
228 | nrdels++; |
229 | } |
230 | } |
231 | } |
232 | } |
233 | |
234 | closedir(dir); |
235 | |
236 | return (nrdels); |
237 | } |
238 | |
239 | static int ps_files_key_exists(ps_files *data, const char *key TSRMLS_DC) |
240 | { |
241 | char buf[MAXPATHLEN]; |
242 | struct stat sbuf; |
243 | |
244 | if (!key || !ps_files_path_create(buf, sizeof(buf), data, key)) { |
245 | return FAILURE; |
246 | } |
247 | if (VCWD_STAT(buf, &sbuf)) { |
248 | return FAILURE; |
249 | } |
250 | return SUCCESS; |
251 | } |
252 | |
253 | |
254 | #define PS_FILES_DATA ps_files *data = PS_GET_MOD_DATA() |
255 | |
256 | PS_OPEN_FUNC(files) |
257 | { |
258 | ps_files *data; |
259 | const char *p, *last; |
260 | const char *argv[3]; |
261 | int argc = 0; |
262 | size_t dirdepth = 0; |
263 | int filemode = 0600; |
264 | |
265 | if (*save_path == '\0') { |
266 | /* if save path is an empty string, determine the temporary dir */ |
267 | save_path = php_get_temporary_directory(TSRMLS_C); |
268 | |
269 | if (php_check_open_basedir(save_path TSRMLS_CC)) { |
270 | return FAILURE; |
271 | } |
272 | } |
273 | |
274 | /* split up input parameter */ |
275 | last = save_path; |
276 | p = strchr(save_path, ';'); |
277 | while (p) { |
278 | argv[argc++] = last; |
279 | last = ++p; |
280 | p = strchr(p, ';'); |
281 | if (argc > 1) break; |
282 | } |
283 | argv[argc++] = last; |
284 | |
285 | if (argc > 1) { |
286 | errno = 0; |
287 | dirdepth = (size_t) strtol(argv[0], NULL, 10); |
288 | if (errno == ERANGE) { |
289 | php_error(E_WARNING, "The first parameter in session.save_path is invalid" ); |
290 | return FAILURE; |
291 | } |
292 | } |
293 | |
294 | if (argc > 2) { |
295 | errno = 0; |
296 | filemode = strtol(argv[1], NULL, 8); |
297 | if (errno == ERANGE || filemode < 0 || filemode > 07777) { |
298 | php_error(E_WARNING, "The second parameter in session.save_path is invalid" ); |
299 | return FAILURE; |
300 | } |
301 | } |
302 | save_path = argv[argc - 1]; |
303 | |
304 | data = ecalloc(1, sizeof(*data)); |
305 | |
306 | data->fd = -1; |
307 | data->dirdepth = dirdepth; |
308 | data->filemode = filemode; |
309 | data->basedir_len = strlen(save_path); |
310 | data->basedir = estrndup(save_path, data->basedir_len); |
311 | |
312 | if (PS_GET_MOD_DATA()) { |
313 | ps_close_files(mod_data TSRMLS_CC); |
314 | } |
315 | PS_SET_MOD_DATA(data); |
316 | |
317 | return SUCCESS; |
318 | } |
319 | |
320 | PS_CLOSE_FUNC(files) |
321 | { |
322 | PS_FILES_DATA; |
323 | |
324 | ps_files_close(data); |
325 | |
326 | if (data->lastkey) { |
327 | efree(data->lastkey); |
328 | data->lastkey = NULL; |
329 | } |
330 | |
331 | efree(data->basedir); |
332 | efree(data); |
333 | *mod_data = NULL; |
334 | |
335 | return SUCCESS; |
336 | } |
337 | |
338 | PS_READ_FUNC(files) |
339 | { |
340 | long n; |
341 | struct stat sbuf; |
342 | PS_FILES_DATA; |
343 | |
344 | /* If strict mode, check session id existence */ |
345 | if (PS(use_strict_mode) && |
346 | ps_files_key_exists(data, key TSRMLS_CC) == FAILURE) { |
347 | /* key points to PS(id), but cannot change here. */ |
348 | if (key) { |
349 | efree(PS(id)); |
350 | PS(id) = NULL; |
351 | } |
352 | PS(id) = PS(mod)->s_create_sid((void **)&data, NULL TSRMLS_CC); |
353 | if (!PS(id)) { |
354 | return FAILURE; |
355 | } |
356 | if (PS(use_cookies)) { |
357 | PS(send_cookie) = 1; |
358 | } |
359 | php_session_reset_id(TSRMLS_C); |
360 | PS(session_status) = php_session_active; |
361 | } |
362 | |
363 | ps_files_open(data, PS(id) TSRMLS_CC); |
364 | if (data->fd < 0) { |
365 | return FAILURE; |
366 | } |
367 | |
368 | if (fstat(data->fd, &sbuf)) { |
369 | return FAILURE; |
370 | } |
371 | |
372 | data->st_size = *vallen = sbuf.st_size; |
373 | |
374 | if (sbuf.st_size == 0) { |
375 | *val = STR_EMPTY_ALLOC(); |
376 | return SUCCESS; |
377 | } |
378 | |
379 | *val = emalloc(sbuf.st_size); |
380 | |
381 | #if defined(HAVE_PREAD) |
382 | n = pread(data->fd, *val, sbuf.st_size, 0); |
383 | #else |
384 | lseek(data->fd, 0, SEEK_SET); |
385 | n = read(data->fd, *val, sbuf.st_size); |
386 | #endif |
387 | |
388 | if (n != sbuf.st_size) { |
389 | if (n == -1) { |
390 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "read failed: %s (%d)" , strerror(errno), errno); |
391 | } else { |
392 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "read returned less bytes than requested" ); |
393 | } |
394 | efree(*val); |
395 | return FAILURE; |
396 | } |
397 | |
398 | return SUCCESS; |
399 | } |
400 | |
401 | PS_WRITE_FUNC(files) |
402 | { |
403 | long n; |
404 | PS_FILES_DATA; |
405 | |
406 | ps_files_open(data, key TSRMLS_CC); |
407 | if (data->fd < 0) { |
408 | return FAILURE; |
409 | } |
410 | |
411 | /* Truncate file if the amount of new data is smaller than the existing data set. */ |
412 | |
413 | if (vallen < (int)data->st_size) { |
414 | php_ignore_value(ftruncate(data->fd, 0)); |
415 | } |
416 | |
417 | #if defined(HAVE_PWRITE) |
418 | n = pwrite(data->fd, val, vallen, 0); |
419 | #else |
420 | lseek(data->fd, 0, SEEK_SET); |
421 | n = write(data->fd, val, vallen); |
422 | #endif |
423 | |
424 | if (n != vallen) { |
425 | if (n == -1) { |
426 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "write failed: %s (%d)" , strerror(errno), errno); |
427 | } else { |
428 | php_error_docref(NULL TSRMLS_CC, E_WARNING, "write wrote less bytes than requested" ); |
429 | } |
430 | return FAILURE; |
431 | } |
432 | |
433 | return SUCCESS; |
434 | } |
435 | |
436 | PS_DESTROY_FUNC(files) |
437 | { |
438 | char buf[MAXPATHLEN]; |
439 | PS_FILES_DATA; |
440 | |
441 | if (!ps_files_path_create(buf, sizeof(buf), data, key)) { |
442 | return FAILURE; |
443 | } |
444 | |
445 | if (data->fd != -1) { |
446 | ps_files_close(data); |
447 | |
448 | if (VCWD_UNLINK(buf) == -1) { |
449 | /* This is a little safety check for instances when we are dealing with a regenerated session |
450 | * that was not yet written to disk. */ |
451 | if (!VCWD_ACCESS(buf, F_OK)) { |
452 | return FAILURE; |
453 | } |
454 | } |
455 | } |
456 | |
457 | return SUCCESS; |
458 | } |
459 | |
460 | PS_GC_FUNC(files) |
461 | { |
462 | PS_FILES_DATA; |
463 | |
464 | /* we don't perform any cleanup, if dirdepth is larger than 0. |
465 | we return SUCCESS, since all cleanup should be handled by |
466 | an external entity (i.e. find -ctime x | xargs rm) */ |
467 | |
468 | if (data->dirdepth == 0) { |
469 | *nrdels = ps_files_cleanup_dir(data->basedir, maxlifetime TSRMLS_CC); |
470 | } |
471 | |
472 | return SUCCESS; |
473 | } |
474 | |
475 | PS_CREATE_SID_FUNC(files) |
476 | { |
477 | char *sid; |
478 | int maxfail = 3; |
479 | PS_FILES_DATA; |
480 | |
481 | do { |
482 | sid = php_session_create_id((void **)&data, newlen TSRMLS_CC); |
483 | /* Check collision */ |
484 | if (data && ps_files_key_exists(data, sid TSRMLS_CC) == SUCCESS) { |
485 | if (sid) { |
486 | efree(sid); |
487 | sid = NULL; |
488 | } |
489 | if (!(maxfail--)) { |
490 | return NULL; |
491 | } |
492 | } |
493 | } while(!sid); |
494 | |
495 | return sid; |
496 | } |
497 | |
498 | |
499 | /* |
500 | * Local variables: |
501 | * tab-width: 4 |
502 | * c-basic-offset: 4 |
503 | * End: |
504 | * vim600: sw=4 ts=4 fdm=marker |
505 | * vim<600: sw=4 ts=4 |
506 | */ |
507 | |