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