1 /*
2   +----------------------------------------------------------------------+
3   | Copyright (c) 2009-2010 The PHP Group                                |
4   +----------------------------------------------------------------------+
5   | This source file is subject to version 3.01 of the PHP license,      |
6   | that is bundled with this package in the file LICENSE, and is        |
7   | available through the world-wide-web at the following url:           |
8   | http://www.php.net/license/3_01.txt.                                 |
9   | If you did not receive a copy of the PHP license and are unable to   |
10   | obtain it through the world-wide-web, please send a note to          |
11   | license@php.net so we can mail you a copy immediately.               |
12   +----------------------------------------------------------------------+
13   | Authors: Andrei Zmievski <andrei@php.net>                            |
14   +----------------------------------------------------------------------+
15 */
16 
17 #include "php_memcached.h"
18 #include "php_memcached_private.h"
19 #include "php_memcached_session.h"
20 
21 #include "Zend/zend_smart_str_public.h"
22 
23 extern ZEND_DECLARE_MODULE_GLOBALS(php_memcached)
24 
25 #define REALTIME_MAXDELTA 60*60*24*30
26 
27 ps_module ps_mod_memcached = {
28 	PS_MOD_UPDATE_TIMESTAMP(memcached)
29 };
30 
31 typedef struct  {
32 	zend_bool is_persistent;
33 	zend_bool has_sasl_data;
34 	zend_bool    is_locked;
35 	zend_string *lock_key;
36 } php_memcached_user_data;
37 
38 #ifndef MIN
39 # define MIN(a,b) (((a)<(b))?(a):(b))
40 #endif
41 
42 #ifndef MAX
43 # define MAX(a,b) (((a)>(b))?(a):(b))
44 #endif
45 
46 static
47 	int le_memc_sess;
48 
49 static
s_memc_sess_list_entry(void)50 int s_memc_sess_list_entry(void)
51 {
52 	return le_memc_sess;
53 }
54 
55 static
s_destroy_mod_data(memcached_st * memc)56 void s_destroy_mod_data(memcached_st *memc)
57 {
58 	php_memcached_user_data *user_data = memcached_get_user_data(memc);
59 
60 #ifdef HAVE_MEMCACHED_SASL
61 	if (user_data->has_sasl_data) {
62 		memcached_destroy_sasl_auth_data(memc);
63 	}
64 #endif
65 
66 	memcached_free(memc);
67 	pefree(memc, user_data->is_persistent);
68 	pefree(user_data, user_data->is_persistent);
69 }
70 
ZEND_RSRC_DTOR_FUNC(php_memc_sess_dtor)71 ZEND_RSRC_DTOR_FUNC(php_memc_sess_dtor)
72 {
73 	if (res->ptr) {
74 		s_destroy_mod_data((memcached_st *) res->ptr);
75 		res->ptr = NULL;
76 	}
77 }
78 
php_memc_session_minit(int module_number)79 int php_memc_session_minit(int module_number)
80 {
81 	le_memc_sess =
82 		zend_register_list_destructors_ex(NULL, php_memc_sess_dtor, "Memcached Sessions persistent connection", module_number);
83 
84 	php_session_register_module(ps_memcached_ptr);
85 	return SUCCESS;
86 }
87 
88 static
s_adjust_expiration(zend_long expiration)89 time_t s_adjust_expiration(zend_long expiration)
90 {
91     if (expiration <= REALTIME_MAXDELTA) {
92         return expiration;
93     } else {
94         return time(NULL) + expiration;
95     }
96 }
97 
98 static
s_lock_expiration()99 time_t s_lock_expiration()
100 {
101 	if (MEMC_SESS_INI(lock_expiration) > 0) {
102 		return s_adjust_expiration(MEMC_SESS_INI(lock_expiration));
103 	}
104 	else {
105 		zend_long max_execution_time = zend_ini_long(ZEND_STRL("max_execution_time"), 0);
106 		if (max_execution_time > 0) {
107 			return s_adjust_expiration(max_execution_time);
108 		}
109 	}
110 	return 0;
111 }
112 
113 static
s_session_expiration(zend_long maxlifetime)114 time_t s_session_expiration(zend_long maxlifetime)
115 {
116 	if (maxlifetime > 0) {
117 		return s_adjust_expiration(maxlifetime);
118 	}
119 	return 0;
120 }
121 
122 static
s_lock_session(memcached_st * memc,zend_string * sid)123 zend_bool s_lock_session(memcached_st *memc, zend_string *sid)
124 {
125 	memcached_return rc;
126 	char *lock_key;
127 	size_t lock_key_len;
128 	time_t expiration;
129 	zend_long wait_time, retries;
130 	php_memcached_user_data *user_data = memcached_get_user_data(memc);
131 
132 	lock_key_len = spprintf(&lock_key, 0, "lock.%s", sid->val);
133 	expiration   = s_lock_expiration();
134 
135 	wait_time = MEMC_SESS_INI(lock_wait_min);
136 	retries   = MEMC_SESS_INI(lock_retries);
137 
138 	do {
139 		rc = memcached_add(memc, lock_key, lock_key_len, "1", sizeof ("1") - 1, expiration, 0);
140 
141 		switch (rc) {
142 
143 			case MEMCACHED_SUCCESS:
144 				user_data->lock_key  = zend_string_init(lock_key, lock_key_len, user_data->is_persistent);
145 				user_data->is_locked = 1;
146 			break;
147 
148 			case MEMCACHED_NOTSTORED:
149 			case MEMCACHED_DATA_EXISTS:
150 				if (retries > 0) {
151 					usleep(wait_time * 1000);
152 					wait_time = MIN(MEMC_SESS_INI(lock_wait_max), wait_time * 2);
153 				}
154 			break;
155 
156 			default:
157 				php_error_docref(NULL, E_WARNING, "Failed to write session lock: %s", memcached_strerror (memc, rc));
158 				break;
159 		}
160 	} while (!user_data->is_locked && retries-- > 0);
161 
162 	efree(lock_key);
163 	return user_data->is_locked;
164 }
165 
166 static
s_unlock_session(memcached_st * memc)167 void s_unlock_session(memcached_st *memc)
168 {
169 	php_memcached_user_data *user_data = memcached_get_user_data(memc);
170 
171 	if (user_data->is_locked) {
172 		memcached_delete(memc, user_data->lock_key->val, user_data->lock_key->len, 0);
173 		user_data->is_locked = 0;
174 		zend_string_release (user_data->lock_key);
175 	}
176 }
177 
178 static
s_configure_from_ini_values(memcached_st * memc,zend_bool silent)179 zend_bool s_configure_from_ini_values(memcached_st *memc, zend_bool silent)
180 {
181 /* This macro looks like a function but returns errors directly */
182 #define check_set_behavior(behavior, value) \
183 { \
184 	int b = (behavior); \
185 	uint64_t v = (value); \
186 	if (v != memcached_behavior_get(memc, b)) { \
187 		memcached_return rc; \
188 		if ((rc = memcached_behavior_set(memc, b, v)) != MEMCACHED_SUCCESS) { \
189 			if (!silent) { \
190 				php_error_docref(NULL, E_WARNING, "failed to initialise session memcached configuration: %s", memcached_strerror(memc, rc)); \
191 			} \
192 			return 0; \
193 		} \
194 	} \
195 }
196 
197 	if (MEMC_SESS_INI(binary_protocol_enabled)) {
198 		check_set_behavior(MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);
199 		/* Also enable TCP_NODELAY when binary protocol is enabled */
200 		check_set_behavior(MEMCACHED_BEHAVIOR_TCP_NODELAY, 1);
201 	}
202 
203 	if (MEMC_SESS_INI(consistent_hash_enabled)) {
204 		check_set_behavior(MEMC_SESS_INI(consistent_hash_type), 1);
205 	}
206 
207 	if (MEMC_SESS_INI(server_failure_limit)) {
208 		check_set_behavior(MEMCACHED_BEHAVIOR_SERVER_FAILURE_LIMIT, MEMC_SESS_INI(server_failure_limit));
209 	}
210 
211 	if (MEMC_SESS_INI(number_of_replicas)) {
212 		check_set_behavior(MEMCACHED_BEHAVIOR_NUMBER_OF_REPLICAS, MEMC_SESS_INI(number_of_replicas));
213 	}
214 
215 	if (MEMC_SESS_INI(randomize_replica_read_enabled)) {
216 		check_set_behavior(MEMCACHED_BEHAVIOR_RANDOMIZE_REPLICA_READ, 1);
217 	}
218 
219 	if (MEMC_SESS_INI(remove_failed_servers_enabled)) {
220 		check_set_behavior(MEMCACHED_BEHAVIOR_REMOVE_FAILED_SERVERS, 1);
221 	}
222 
223 	if (MEMC_SESS_INI(connect_timeout)) {
224 		check_set_behavior(MEMCACHED_BEHAVIOR_CONNECT_TIMEOUT, MEMC_SESS_INI(connect_timeout));
225 	}
226 
227 	if (MEMC_SESS_STR_INI(prefix)) {
228 		memcached_callback_set(memc, MEMCACHED_CALLBACK_NAMESPACE, MEMC_SESS_STR_INI(prefix));
229 	}
230 
231 	if (MEMC_SESS_STR_INI(sasl_username) && MEMC_SESS_STR_INI(sasl_password)) {
232 		php_memcached_user_data *user_data;
233 
234 		if (!php_memc_init_sasl_if_needed()) {
235 			return 0;
236 		}
237 
238 		check_set_behavior(MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);
239 
240 		if (memcached_set_sasl_auth_data(memc, MEMC_SESS_STR_INI(sasl_username), MEMC_SESS_STR_INI(sasl_password)) == MEMCACHED_FAILURE) {
241 			php_error_docref(NULL, E_WARNING, "failed to set memcached session sasl credentials");
242 			return 0;
243 		}
244 		user_data = memcached_get_user_data(memc);
245 		user_data->has_sasl_data = 1;
246 	}
247 
248 #undef check_set_behavior
249 
250 	return 1;
251 }
252 
253 static
s_pemalloc_fn(const memcached_st * memc,size_t size,void * context)254 void *s_pemalloc_fn(const memcached_st *memc, size_t size, void *context)
255 {
256 	zend_bool *is_persistent = memcached_get_user_data(memc);
257 
258 	return
259 		pemalloc(size, *is_persistent);
260 }
261 
262 static
s_pefree_fn(const memcached_st * memc,void * mem,void * context)263 void s_pefree_fn(const memcached_st *memc, void *mem, void *context)
264 {
265 	zend_bool *is_persistent = memcached_get_user_data(memc);
266 
267 	return
268 		pefree(mem, *is_persistent);
269 }
270 
271 static
s_perealloc_fn(const memcached_st * memc,void * mem,const size_t size,void * context)272 void *s_perealloc_fn(const memcached_st *memc, void *mem, const size_t size, void *context)
273 {
274 	zend_bool *is_persistent = memcached_get_user_data(memc);
275 
276 	return
277 		perealloc(mem, size, *is_persistent);
278 }
279 
280 static
s_pecalloc_fn(const memcached_st * memc,size_t nelem,const size_t elsize,void * context)281 void *s_pecalloc_fn(const memcached_st *memc, size_t nelem, const size_t elsize, void *context)
282 {
283 	zend_bool *is_persistent = memcached_get_user_data(memc);
284 
285 	return
286 		pecalloc(nelem, elsize, *is_persistent);
287 }
288 
289 
290 static
s_init_mod_data(const memcached_server_list_st servers,zend_bool is_persistent)291 memcached_st *s_init_mod_data (const memcached_server_list_st servers, zend_bool is_persistent)
292 {
293 	void *buffer;
294 	php_memcached_user_data *user_data;
295 	memcached_st *memc;
296 
297 	buffer = pecalloc(1, sizeof(memcached_st), is_persistent);
298 	memc   = memcached_create (buffer);
299 
300 	if (!memc) {
301 		php_error_docref(NULL, E_ERROR, "failed to allocate memcached structure");
302 		/* not reached */
303 	}
304 
305 	memcached_set_memory_allocators(memc, s_pemalloc_fn, s_pefree_fn, s_perealloc_fn, s_pecalloc_fn, NULL);
306 
307 	user_data                = pecalloc(1, sizeof(php_memcached_user_data), is_persistent);
308 	user_data->is_persistent = is_persistent;
309 	user_data->has_sasl_data = 0;
310 	user_data->lock_key      = NULL;
311 	user_data->is_locked     = 0;
312 
313 	memcached_set_user_data(memc, user_data);
314 	memcached_server_push (memc, servers);
315 	memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_VERIFY_KEY, 1);
316 	return memc;
317 }
318 
PS_OPEN_FUNC(memcached)319 PS_OPEN_FUNC(memcached)
320 {
321 	memcached_st *memc   = NULL;
322 	char *plist_key      = NULL;
323 	size_t plist_key_len = 0;
324 
325 	memcached_server_list_st servers;
326 
327 	// Fail on incompatible PERSISTENT identifier (removed in php-memcached 3.0)
328 	if (strstr(save_path, "PERSISTENT=")) {
329 		php_error_docref(NULL, E_WARNING, "failed to parse session.save_path: PERSISTENT is replaced by memcached.sess_persistent = On");
330 		PS_SET_MOD_DATA(NULL);
331 		return FAILURE;
332 	}
333 
334 	// First parse servers
335 	servers = memcached_servers_parse(save_path);
336 
337 	if (!servers) {
338 		php_error_docref(NULL, E_WARNING, "failed to parse session.save_path");
339 		PS_SET_MOD_DATA(NULL);
340 		return FAILURE;
341 	}
342 
343 	if (MEMC_SESS_INI(persistent_enabled)) {
344 		zend_resource *le_p;
345 
346 		plist_key_len = spprintf(&plist_key, 0, "memc-session:%s", save_path);
347 
348 		if ((le_p = zend_hash_str_find_ptr(&EG(persistent_list), plist_key, plist_key_len)) != NULL) {
349 			if (le_p->type == s_memc_sess_list_entry()) {
350 				memc = (memcached_st *) le_p->ptr;
351 
352 				if (!s_configure_from_ini_values(memc, 1)) {
353 					// Remove existing plist entry
354 					zend_hash_str_del(&EG(persistent_list), plist_key, plist_key_len);
355 					memc = NULL;
356 				}
357 				else {
358 					efree(plist_key);
359 					PS_SET_MOD_DATA(memc);
360 					memcached_server_list_free(servers);
361 					return SUCCESS;
362 				}
363 			}
364 		}
365 	}
366 
367 	memc = s_init_mod_data(servers, MEMC_SESS_INI(persistent_enabled));
368 	memcached_server_list_free(servers);
369 
370 	if (!s_configure_from_ini_values(memc, 0)) {
371 		if (plist_key) {
372 			efree(plist_key);
373 		}
374 		s_destroy_mod_data(memc);
375 		PS_SET_MOD_DATA(NULL);
376 		return FAILURE;
377 	}
378 
379 	if (plist_key) {
380 		zend_resource le;
381 
382 		le.type = s_memc_sess_list_entry();
383 		le.ptr  = memc;
384 
385 		GC_SET_REFCOUNT(&le, 1);
386 
387 		/* plist_key is not a persistent allocated key, thus we use str_update here */
388 		if (zend_hash_str_update_mem(&EG(persistent_list), plist_key, plist_key_len, &le, sizeof(le)) == NULL) {
389 			php_error_docref(NULL, E_ERROR, "Could not register persistent entry for the memcached session");
390 			/* not reached */
391 		}
392 		efree(plist_key);
393 	}
394 	PS_SET_MOD_DATA(memc);
395 	return SUCCESS;
396 }
397 
PS_CLOSE_FUNC(memcached)398 PS_CLOSE_FUNC(memcached)
399 {
400 	php_memcached_user_data *user_data;
401 	memcached_st *memc = PS_GET_MOD_DATA();
402 
403 	if (!memc) {
404 		php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
405 		return FAILURE;
406 	}
407 
408 	user_data = memcached_get_user_data(memc);
409 
410 	if (user_data->is_locked) {
411 		s_unlock_session(memc);
412 	}
413 
414 	if (!user_data->is_persistent) {
415 		s_destroy_mod_data(memc);
416 	}
417 
418 	PS_SET_MOD_DATA(NULL);
419 	return SUCCESS;
420 }
421 
PS_READ_FUNC(memcached)422 PS_READ_FUNC(memcached)
423 {
424 	char *payload = NULL;
425 	size_t payload_len = 0;
426 	uint32_t flags = 0;
427 	memcached_return status;
428 	memcached_st *memc = PS_GET_MOD_DATA();
429 
430 	if (!memc) {
431 		php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
432 		return FAILURE;
433 	}
434 
435 	if (MEMC_SESS_INI(lock_enabled)) {
436 		if (!s_lock_session(memc, key)) {
437 			php_error_docref(NULL, E_WARNING, "Unable to clear session lock record");
438 			return FAILURE;
439 		}
440 	}
441 
442 	payload = memcached_get(memc, key->val, key->len, &payload_len, &flags, &status);
443 
444 	if (status == MEMCACHED_SUCCESS) {
445 		zend_bool *is_persistent = memcached_get_user_data(memc);
446 		*val = zend_string_init(payload, payload_len, 0);
447 		pefree(payload, *is_persistent);
448 		return SUCCESS;
449 	} else if (status == MEMCACHED_NOTFOUND) {
450 		*val = ZSTR_EMPTY_ALLOC();
451 		return SUCCESS;
452 	} else {
453 		php_error_docref(NULL, E_WARNING, "error getting session from memcached: %s", memcached_last_error_message(memc));
454 		return FAILURE;
455 	}
456 }
457 
PS_WRITE_FUNC(memcached)458 PS_WRITE_FUNC(memcached)
459 {
460 	zend_long retries = 1;
461 	memcached_st *memc = PS_GET_MOD_DATA();
462 	time_t expiration = s_session_expiration(maxlifetime);
463 
464 	if (!memc) {
465 		php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
466 		return FAILURE;
467 	}
468 
469 	/* Set the number of write retry attempts to the number of replicas times the number of attempts to remove a server plus the initial write */
470 	if (MEMC_SESS_INI(remove_failed_servers_enabled)) {
471 		zend_long replicas, failure_limit;
472 
473 		replicas = memcached_behavior_get(memc, MEMCACHED_BEHAVIOR_NUMBER_OF_REPLICAS);
474 		failure_limit = memcached_behavior_get(memc, MEMCACHED_BEHAVIOR_SERVER_FAILURE_LIMIT);
475 
476 		retries = 1 + replicas * (failure_limit + 1);
477 	}
478 
479 	do {
480 		if (memcached_set(memc, key->val, key->len, val->val, val->len, expiration, 0) == MEMCACHED_SUCCESS) {
481 			return SUCCESS;
482 		} else {
483 			php_error_docref(NULL, E_WARNING, "error saving session to memcached: %s", memcached_last_error_message(memc));
484 		}
485 	} while (--retries > 0);
486 
487 	return FAILURE;
488 }
489 
PS_DESTROY_FUNC(memcached)490 PS_DESTROY_FUNC(memcached)
491 {
492 	php_memcached_user_data *user_data;
493 	memcached_st *memc = PS_GET_MOD_DATA();
494 
495 	if (!memc) {
496 		php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
497 		return FAILURE;
498 	}
499 
500 	memcached_delete(memc, key->val, key->len, 0);
501 	user_data = memcached_get_user_data(memc);
502 
503 	if (user_data->is_locked) {
504 		s_unlock_session(memc);
505 	}
506 	return SUCCESS;
507 }
508 
PS_GC_FUNC(memcached)509 PS_GC_FUNC(memcached)
510 {
511 	return SUCCESS;
512 }
513 
PS_CREATE_SID_FUNC(memcached)514 PS_CREATE_SID_FUNC(memcached)
515 {
516 	zend_string *sid;
517 	memcached_st *memc = PS_GET_MOD_DATA();
518 
519 	if (!memc) {
520 		sid = php_session_create_id(NULL);
521 	}
522 	else {
523 		int retries = 3;
524 		while (retries-- > 0) {
525 			sid = php_session_create_id((void **) &memc);
526 
527 			if (memcached_add (memc, sid->val, sid->len, NULL, 0, s_lock_expiration(), 0) == MEMCACHED_SUCCESS) {
528 				break;
529 			}
530 			zend_string_release(sid);
531 			sid = NULL;
532 		}
533 	}
534 	return sid;
535 }
536 
PS_VALIDATE_SID_FUNC(memcached)537 PS_VALIDATE_SID_FUNC(memcached)
538 {
539 	memcached_st *memc = PS_GET_MOD_DATA();
540 
541 	if (php_memcached_exist(memc, key) == MEMCACHED_SUCCESS) {
542 		return SUCCESS;
543 	} else {
544 		return FAILURE;
545 	}
546 }
547 
PS_UPDATE_TIMESTAMP_FUNC(memcached)548 PS_UPDATE_TIMESTAMP_FUNC(memcached)
549 {
550 	memcached_st *memc = PS_GET_MOD_DATA();
551 	time_t expiration = s_session_expiration(maxlifetime);
552 
553 	if (php_memcached_touch(memc, key->val, key->len, expiration) == MEMCACHED_FAILURE) {
554 		return FAILURE;
555 	}
556 	return SUCCESS;
557 }
558 /* }}} */
559 
560