/* +----------------------------------------------------------------------+ | HipHop for PHP | +----------------------------------------------------------------------+ | Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) | | Copyright (c) 1997-2010 The PHP Group | +----------------------------------------------------------------------+ | This source file is subject to version 3.01 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available through the world-wide-web at the following url: | | http://www.php.net/license/3_01.txt | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ */ #include "hphp/runtime/ext/ext_session.h" #include "hphp/runtime/ext/ext_options.h" #include "hphp/runtime/ext/ext_hash.h" #include "hphp/runtime/ext/ext_function.h" #include "hphp/runtime/base/builtin_functions.h" #include "hphp/runtime/base/zend_math.h" #include "hphp/runtime/base/string_buffer.h" #include "hphp/runtime/base/request_local.h" #include "hphp/runtime/base/ini_setting.h" #include "hphp/runtime/base/datetime.h" #include "hphp/runtime/base/variable_unserializer.h" #include "hphp/runtime/base/array_iterator.h" #include "hphp/util/lock.h" #include "hphp/util/logger.h" #include "hphp/util/compatibility.h" #include #include #include #include namespace HPHP { IMPLEMENT_DEFAULT_EXTENSION(session); /////////////////////////////////////////////////////////////////////////////// bool ini_on_update_save_handler(CStrRef value, void *p); bool ini_on_update_serializer(CStrRef value, void *p); bool ini_on_update_trans_sid(CStrRef value, void *p); bool ini_on_update_save_dir(CStrRef value, void *p); /////////////////////////////////////////////////////////////////////////////// // global data class SessionSerializer; class Session { public: enum Status { Disabled, None, Active }; std::string m_save_path; std::string m_session_name; std::string m_extern_referer_chk; std::string m_entropy_file; int64_t m_entropy_length; std::string m_cache_limiter; int64_t m_cookie_lifetime; std::string m_cookie_path; std::string m_cookie_domain; bool m_cookie_secure; bool m_cookie_httponly; SessionModule *m_mod; Status m_session_status; int64_t m_gc_probability; int64_t m_gc_divisor; int64_t m_gc_maxlifetime; int m_module_number; int64_t m_cache_expire; Object m_ps_session_handler; SessionSerializer *m_serializer; bool m_auto_start; bool m_use_cookies; bool m_use_only_cookies; bool m_use_trans_sid; // contains the INI value of whether to use trans-sid bool m_apply_trans_sid; // whether to enable trans-sid for current request std::string m_hash_func; int64_t m_hash_bits_per_character; int m_send_cookie; int m_define_sid; bool m_invalid_session_id; /* allows the driver to report about an invalid session id and request id regeneration */ Session() : m_entropy_length(0), m_cookie_lifetime(0), m_cookie_secure(false), m_cookie_httponly(false), m_mod(NULL), m_session_status(None), m_gc_probability(0), m_gc_divisor(0), m_gc_maxlifetime(0), m_module_number(0), m_cache_expire(0), m_serializer(NULL), m_auto_start(false), m_use_cookies(false), m_use_only_cookies(false), m_use_trans_sid(false), m_apply_trans_sid(false), m_hash_bits_per_character(0), m_send_cookie(0), m_define_sid(0), m_invalid_session_id(false) { } }; class SessionRequestData : public RequestEventHandler, public Session { public: SessionRequestData() : m_threadInited(false) {} virtual void requestInit() { if (!m_threadInited) { m_threadInited = true; threadInit(); } m_id.reset(); m_session_status = Session::None; } void requestShutdownImpl(); virtual void requestShutdown() { f_session_write_close(); requestShutdownImpl(); } public: bool m_threadInited; String m_id; void threadInit() { IniSetting::Bind("session.save_path", "", ini_on_update_save_dir, &m_save_path); IniSetting::Bind("session.name", "PHPSESSID", ini_on_update_string, &m_session_name); IniSetting::Bind("session.save_handler", "files", ini_on_update_save_handler); IniSetting::Bind("session.auto_start", "0", ini_on_update_bool, &m_auto_start); IniSetting::Bind("session.gc_probability", "1", ini_on_update_long, &m_gc_probability); IniSetting::Bind("session.gc_divisor", "100", ini_on_update_long, &m_gc_divisor); IniSetting::Bind("session.gc_maxlifetime", "1440", ini_on_update_long, &m_gc_maxlifetime); IniSetting::Bind("session.serialize_handler", "php", ini_on_update_serializer); IniSetting::Bind("session.cookie_lifetime", "0", ini_on_update_long, &m_cookie_lifetime); IniSetting::Bind("session.cookie_path", "/", ini_on_update_string, &m_cookie_path); IniSetting::Bind("session.cookie_domain", "", ini_on_update_string, &m_cookie_domain); IniSetting::Bind("session.cookie_secure", "", ini_on_update_bool, &m_cookie_secure); IniSetting::Bind("session.cookie_httponly", "", ini_on_update_bool, &m_cookie_httponly); IniSetting::Bind("session.use_cookies", "1", ini_on_update_bool, &m_use_cookies); IniSetting::Bind("session.use_only_cookies", "1", ini_on_update_bool, &m_use_only_cookies); IniSetting::Bind("session.referer_check", "", ini_on_update_string, &m_extern_referer_chk); IniSetting::Bind("session.entropy_file", "", ini_on_update_string, &m_entropy_file); IniSetting::Bind("session.entropy_length", "0", ini_on_update_long, &m_entropy_length); IniSetting::Bind("session.cache_limiter", "nocache", ini_on_update_string, &m_cache_limiter); IniSetting::Bind("session.cache_expire", "180", ini_on_update_long, &m_cache_expire); IniSetting::Bind("session.use_trans_sid", "0", ini_on_update_trans_sid); IniSetting::Bind("session.hash_function", "0", ini_on_update_string, &m_hash_func); IniSetting::Bind("session.hash_bits_per_character", "4", ini_on_update_long, &m_hash_bits_per_character); } }; IMPLEMENT_STATIC_REQUEST_LOCAL(SessionRequestData, s_session); #define PS(name) s_session->m_ ## name void SessionRequestData::requestShutdownImpl() { if (m_mod) { try { m_mod->close(); } catch (...) {} } m_id.reset(); } std::vector SessionModule::RegisteredModules; /* * Note that we cannot use the BASE64 alphabet here, because * it contains "/" and "+": both are unacceptable for simple inclusion * into URLs. */ static char hexconvtab[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,-"; /* returns a pointer to the byte after the last valid character in out */ static void bin_to_readable(CStrRef in, StringBuffer &out, char nbits) { unsigned char *p = (unsigned char *)in.data(); unsigned char *q = (unsigned char *)in.data() + in.size(); unsigned short w = 0; int have = 0; int mask = (1 << nbits) - 1; while (true) { if (have < nbits) { if (p < q) { w |= *p++ << have; have += 8; } else { /* consumed everything? */ if (have == 0) break; /* No? We need a final round */ have = nbits; } } /* consume nbits */ out.append(hexconvtab[w & mask]); w >>= nbits; have -= nbits; } } static const StaticString s_REMOTE_ADDR("REMOTE_ADDR"), s__SERVER("_SERVER"), s__SESSION("_SESSION"), s__COOKIE("_COOKIE"), s__GET("_GET"), s__POST("_POST"); String SessionModule::create_sid() { GlobalVariables *g = get_global_variables(); String remote_addr = g->get(s__SERVER)[s_REMOTE_ADDR].toString(); struct timeval tv; gettimeofday(&tv, NULL); StringBuffer buf; buf.printf("%.15s%ld%ld%0.8F", remote_addr.data(), tv.tv_sec, (long int)tv.tv_usec, math_combined_lcg() * 10); if (String(PS(hash_func)).isNumeric()) { switch (String(PS(hash_func)).toInt64()) { case md5: PS(hash_func) = "md5"; break; case sha1: PS(hash_func) = "sha1"; break; } } Variant context = f_hash_init(PS(hash_func)); if (same(context, false)) { Logger::Error("Invalid session hash function: %s", PS(hash_func).c_str()); return String(); } if (!f_hash_update(context.toResource(), buf.detach())) { Logger::Error("hash_update() failed"); return String(); } if (PS(entropy_length) > 0) { int fd = open(PS(entropy_file).c_str(), O_RDONLY); if (fd >= 0) { unsigned char rbuf[2048]; int n; int to_read = PS(entropy_length); while (to_read > 0) { n = ::read(fd, rbuf, (to_read < (int)sizeof(rbuf) ? to_read : (int)sizeof(buf))); if (n <= 0) break; if (!f_hash_update(context.toResource(), String((const char *)rbuf, n, AttachLiteral))) { Logger::Error("hash_update() failed"); ::close(fd); return String(); } to_read -= n; } ::close(fd); } } String hashed = f_hash_final(context.toResource()); if (PS(hash_bits_per_character) < 4 || PS(hash_bits_per_character) > 6) { PS(hash_bits_per_character) = 4; raise_warning("The ini setting hash_bits_per_character is out of range " "(should be 4, 5, or 6) - using 4 for now"); } StringBuffer readable; bin_to_readable(hashed, readable, PS(hash_bits_per_character)); return readable.detach(); } /////////////////////////////////////////////////////////////////////////////// // FileSessionModule class FileSessionData { public: FileSessionData() : m_fd(-1), m_dirdepth(0), m_st_size(0), m_filemode(0600) { } bool open(const char *save_path, const char *session_name) { String tmpdir; if (*save_path == '\0') { tmpdir = f_sys_get_temp_dir(); save_path = tmpdir.data(); } /* split up input parameter */ const char *argv[3]; int argc = 0; const char *last = save_path; const char *p = strchr(save_path, ';'); while (p) { argv[argc++] = last; last = ++p; p = strchr(p, ';'); if (argc > 1) break; } argv[argc++] = last; if (argc > 1) { errno = 0; m_dirdepth = (size_t) strtol(argv[0], NULL, 10); if (errno == ERANGE) { raise_warning("The first parameter in session.save_path is invalid"); return false; } } if (argc > 2) { errno = 0; m_filemode = strtol(argv[1], NULL, 8); if (errno == ERANGE || m_filemode < 0 || m_filemode > 07777) { raise_warning("The second parameter in session.save_path is invalid"); return false; } } save_path = argv[argc - 1]; if (File::TranslatePath(save_path).empty()) { raise_warning("Unable to open save_path %s", save_path); return false; } m_fd = -1; m_basedir = save_path; return true; } bool close() { closeImpl(); m_lastkey.clear(); m_basedir.clear(); return true; } bool read(const char *key, String &value) { openImpl(key); if (m_fd < 0) { return false; } struct stat sbuf; if (fstat(m_fd, &sbuf)) { return false; } m_st_size = sbuf.st_size; if (m_st_size == 0) { value = ""; return true; } String s = String(m_st_size, ReserveString); char *val = s.mutableSlice().ptr; #if defined(HAVE_PREAD) long n = pread(m_fd, val, m_st_size, 0); #else lseek(m_fd, 0, SEEK_SET); long n = ::read(m_fd, val, m_st_size); #endif if (n != (int)m_st_size) { if (n == -1) { raise_warning("read failed: %s (%d)", strerror(errno), errno); } else { raise_warning("read returned less bytes than requested"); } return false; } value = s.setSize(m_st_size); return true; } bool write(const char *key, CStrRef value) { openImpl(key); if (m_fd < 0) { return false; } struct stat sbuf; if (fstat(m_fd, &sbuf)) { return false; } m_st_size = sbuf.st_size; /* * truncate file, if the amount of new data is smaller than * the existing data set. */ if (value.size() < (int)m_st_size) { if (ftruncate(m_fd, 0) < 0) { raise_warning("truncate failed: %s (%d)", strerror(errno), errno); return false; } } #if defined(HAVE_PWRITE) long n = pwrite(m_fd, value.data(), value.size(), 0); #else lseek(m_fd, 0, SEEK_SET); long n = ::write(m_fd, value.data(), value.size()); #endif if (n != value.size()) { if (n == -1) { raise_warning("write failed: %s (%d)", strerror(errno), errno); } else { raise_warning("write wrote less bytes than requested"); } return false; } return true; } bool destroy(const char *key) { char buf[PATH_MAX]; if (!createPath(buf, sizeof(buf), key)) { return false; } if (m_fd != -1) { closeImpl(); if (unlink(buf) == -1) { /* This is a little safety check for instances when we are dealing with a regenerated session that was not yet written to disk */ if (!access(buf, F_OK)) { return false; } } } return true; } bool gc(int maxlifetime, int *nrdels) { /* we don't perform any cleanup, if dirdepth is larger than 0. we return true, since all cleanup should be handled by an external entity (i.e. find -ctime x | xargs rm) */ if (m_dirdepth == 0) { *nrdels = CleanupDir(m_basedir.c_str(), maxlifetime); } return true; } private: int m_fd; string m_lastkey; string m_basedir; size_t m_dirdepth; size_t m_st_size; int m_filemode; /* If you change the logic here, please also update the error message in * ps_files_open() appropriately */ static bool IsValid(const char *key) { const char *p; char c; bool ret = true; for (p = key; (c = *p); p++) { /* valid characters are a..z,A..Z,0..9 */ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == ',' || c == '-')) { ret = false; break; } } size_t len = p - key; if (len == 0) { ret = false; } return ret; } #define FILE_PREFIX "sess_" bool createPath(char *buf, size_t buflen, const char *key) { size_t key_len = strlen(key); if (key_len <= m_dirdepth || buflen < (m_basedir.size() + 2 * m_dirdepth + key_len + 5 + sizeof(FILE_PREFIX))) { return false; } const char *p = key; int n = m_basedir.size(); memcpy(buf, m_basedir.c_str(), n); buf[n++] = PHP_DIR_SEPARATOR; for (int i = 0; i < (int)m_dirdepth; i++) { buf[n++] = *p++; buf[n++] = PHP_DIR_SEPARATOR; } memcpy(buf + n, FILE_PREFIX, sizeof(FILE_PREFIX) - 1); n += sizeof(FILE_PREFIX) - 1; memcpy(buf + n, key, key_len); n += key_len; buf[n] = '\0'; return true; } #ifndef O_BINARY #define O_BINARY 0 #endif void closeImpl() { if (m_fd != -1) { #ifdef PHP_WIN32 /* On Win32 locked files that are closed without being explicitly unlocked will be unlocked only when "system resources become available". */ flock(m_fd, LOCK_UN); #endif ::close(m_fd); m_fd = -1; } } void openImpl(const char *key) { if (m_fd < 0 || !m_lastkey.empty() || m_lastkey != key) { m_lastkey.clear(); closeImpl(); if (!IsValid(key)) { raise_warning("The session id contains illegal characters, " "valid characters are a-z, A-Z, 0-9 and '-,'"); PS(invalid_session_id) = true; return; } char buf[PATH_MAX]; if (!createPath(buf, sizeof(buf), key)) { return; } m_lastkey = key; m_fd = ::open(buf, O_CREAT | O_RDWR | O_BINARY, m_filemode); if (m_fd != -1) { #ifdef PHP_WIN32 flock(m_fd, LOCK_EX); #endif #ifdef F_SETFD # ifndef FD_CLOEXEC # define FD_CLOEXEC 1 # endif if (fcntl(m_fd, F_SETFD, FD_CLOEXEC)) { raise_warning("fcntl(%d, F_SETFD, FD_CLOEXEC) failed: %s (%d)", m_fd, strerror(errno), errno); } #endif } else { raise_warning("open(%s, O_RDWR) failed: %s (%d)", buf, strerror(errno), errno); } } } static int CleanupDir(const char *dirname, int maxlifetime) { DIR *dir = opendir(dirname); if (!dir) { raise_notice("ps_files_cleanup_dir: opendir(%s) failed: %s (%d)", dirname, strerror(errno), errno); return 0; } time_t now; time(&now); size_t dirname_len = strlen(dirname); char dentry[sizeof(struct dirent) + PATH_MAX]; struct dirent *entry = (struct dirent *) &dentry; struct stat sbuf; int nrdels = 0; /* Prepare buffer (dirname never changes) */ char buf[PATH_MAX]; memcpy(buf, dirname, dirname_len); buf[dirname_len] = PHP_DIR_SEPARATOR; while (readdir_r(dir, (struct dirent *)dentry, &entry) == 0 && entry) { /* does the file start with our prefix? */ if (!strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1)) { size_t entry_len = strlen(entry->d_name); /* does it fit into our buffer? */ if (entry_len + dirname_len + 2 < PATH_MAX) { /* create the full path.. */ memcpy(buf + dirname_len + 1, entry->d_name, entry_len); /* NUL terminate it and */ buf[dirname_len + entry_len + 1] = '\0'; /* check whether its last access was more than maxlifet ago */ if (stat(buf, &sbuf) == 0 && (now - sbuf.st_mtime) > maxlifetime) { unlink(buf); nrdels++; } } } } closedir(dir); return nrdels; } }; IMPLEMENT_THREAD_LOCAL(FileSessionData, s_file_session_data); class FileSessionModule : public SessionModule { public: FileSessionModule() : SessionModule("files") { } virtual bool open(const char *save_path, const char *session_name) { return s_file_session_data->open(save_path, session_name); } virtual bool close() { return s_file_session_data->close(); } virtual bool read(const char *key, String &value) { return s_file_session_data->read(key, value); } virtual bool write(const char *key, CStrRef value) { return s_file_session_data->write(key, value); } virtual bool destroy(const char *key) { return s_file_session_data->destroy(key); } virtual bool gc(int maxlifetime, int *nrdels) { return s_file_session_data->gc(maxlifetime, nrdels); } }; static FileSessionModule s_file_session_module; /////////////////////////////////////////////////////////////////////////////// // UserSessionModule class UserSessionModule : public SessionModule { public: UserSessionModule() : SessionModule("user") {} virtual bool open(const char *save_path, const char *session_name) { return vm_call_user_func( CREATE_VECTOR2(PS(ps_session_handler), String("open")), CREATE_VECTOR2(String(save_path, CopyString), String(session_name, CopyString))).toBoolean(); } virtual bool close() { return vm_call_user_func( CREATE_VECTOR2(PS(ps_session_handler), String("close")), Array::Create()).toBoolean(); } virtual bool read(const char *key, String &value) { Variant ret = vm_call_user_func( CREATE_VECTOR2(PS(ps_session_handler), String("read")), CREATE_VECTOR1(String(key, CopyString))); if (ret.isString()) { value = ret.toString(); return true; } return false; } virtual bool write(const char *key, CStrRef value) { return vm_call_user_func( CREATE_VECTOR2(PS(ps_session_handler), String("write")), CREATE_VECTOR2(String(key, CopyString), value)).toBoolean(); } virtual bool destroy(const char *key) { return vm_call_user_func( CREATE_VECTOR2(PS(ps_session_handler), String("destroy")), CREATE_VECTOR1(String(key, CopyString))).toBoolean(); } virtual bool gc(int maxlifetime, int *nrdels) { return vm_call_user_func( CREATE_VECTOR2(PS(ps_session_handler), String("gc")), CREATE_VECTOR1((int64_t)maxlifetime)).toBoolean(); } }; static UserSessionModule s_user_session_module; /////////////////////////////////////////////////////////////////////////////// // session serializers class SessionSerializer { public: explicit SessionSerializer(const char *name) : m_name(name) { RegisteredSerializers.push_back(this); } virtual ~SessionSerializer() {} virtual String encode() = 0; virtual bool decode(CStrRef value) = 0; static SessionSerializer *Find(const char *name) { for (unsigned int i = 0; i < RegisteredSerializers.size(); i++) { SessionSerializer *ss = RegisteredSerializers[i]; if (ss && strcasecmp(name, ss->m_name) == 0) { return ss; } } return NULL; } private: static std::vector RegisteredSerializers; const char *m_name; }; std::vector SessionSerializer::RegisteredSerializers; #define PS_BIN_NR_OF_BITS 8 #define PS_BIN_UNDEF (1<<(PS_BIN_NR_OF_BITS-1)) #define PS_BIN_MAX (PS_BIN_UNDEF-1) class BinarySessionSerializer : public SessionSerializer { public: BinarySessionSerializer() : SessionSerializer("php_binary") {} virtual String encode() { StringBuffer buf; GlobalVariables *g = get_global_variables(); for (ArrayIter iter(g->get(s__SESSION).toArray()); iter; ++iter) { Variant key = iter.first(); if (key.isString()) { String skey = key.toString(); if (skey.size() <= PS_BIN_MAX) { buf.append((unsigned char)skey.size()); buf.append(skey); buf.append(f_serialize(iter.second())); } } else { raise_notice("Skipping numeric key %" PRId64, key.toInt64()); } } return buf.detach(); } virtual bool decode(CStrRef value) { const char *endptr = value.data() + value.size(); GlobalVariables *g = get_global_variables(); for (const char *p = value.data(); p < endptr; ) { int namelen = ((unsigned char)(*p)) & (~PS_BIN_UNDEF); if (namelen < 0 || namelen > PS_BIN_MAX || (p + namelen) >= endptr) { return false; } int has_value = *p & PS_BIN_UNDEF ? 0 : 1; String key(p + 1, namelen, CopyString); p += namelen + 1; if (has_value) { VariableUnserializer vu(p, endptr, VariableUnserializer::Type::Serialize); try { g->getRef(s__SESSION).set(key, vu.unserialize()); p = vu.head(); } catch (Exception &e) { } } } return true; } }; static BinarySessionSerializer s_binary_session_serializer; #define PS_DELIMITER '|' #define PS_UNDEF_MARKER '!' class PhpSessionSerializer : public SessionSerializer { public: PhpSessionSerializer() : SessionSerializer("php") {} virtual String encode() { StringBuffer buf; GlobalVariables *g = get_global_variables(); for (ArrayIter iter(g->get(s__SESSION).toArray()); iter; ++iter) { Variant key = iter.first(); if (key.isString()) { String skey = key.toString(); buf.append(skey); if (skey.find(PS_DELIMITER) >= 0) { return String(); } buf.append(PS_DELIMITER); buf.append(f_serialize(iter.second())); } else { raise_notice("Skipping numeric key %" PRId64, key.toInt64()); } } return buf.detach(); } virtual bool decode(CStrRef value) { const char *p = value.data(); const char *endptr = value.data() + value.size(); GlobalVariables *g = get_global_variables(); while (p < endptr) { const char *q = p; while (*q != PS_DELIMITER) { if (++q >= endptr) return true; } int has_value; if (p[0] == PS_UNDEF_MARKER) { p++; has_value = 0; } else { has_value = 1; } String key(p, q - p, CopyString); q++; if (has_value) { VariableUnserializer vu(q, endptr, VariableUnserializer::Type::Serialize); try { g->getRef(s__SESSION).set(key, vu.unserialize()); q = vu.head(); } catch (Exception &e) { } } p = q; } return true; } }; static PhpSessionSerializer s_php_session_serializer; /////////////////////////////////////////////////////////////////////////////// #define SESSION_CHECK_ACTIVE_STATE \ if (PS(session_status) == Session::Active) { \ raise_warning("A session is active. You cannot change the session" \ " module's ini settings at this time"); \ return false; \ } bool ini_on_update_save_handler(CStrRef value, void *p) { SESSION_CHECK_ACTIVE_STATE; PS(mod) = SessionModule::Find(value.data()); return true; } bool ini_on_update_serializer(CStrRef value, void *p) { SESSION_CHECK_ACTIVE_STATE; PS(serializer) = SessionSerializer::Find(value.data()); return true; } bool ini_on_update_trans_sid(CStrRef value, void *p) { SESSION_CHECK_ACTIVE_STATE; if (!strncasecmp(value.data(), "on", sizeof("on"))) { PS(use_trans_sid) = true; } else { PS(use_trans_sid) = value.toBoolean(); } return true; } bool ini_on_update_save_dir(CStrRef value, void *p) { if (value.find('\0') >= 0) { return false; } const char *path = value.data() + value.rfind(';') + 1; if (File::TranslatePath(path).empty()) { return false; } return ini_on_update_string(value, p); } /////////////////////////////////////////////////////////////////////////////// static int php_session_destroy() { int retval = true; if (PS(session_status) != Session::Active) { raise_warning("Trying to destroy uninitialized session"); return false; } if (PS(mod)->destroy(PS(id).data()) == false) { retval = false; raise_warning("Session object destruction failed"); } s_session->requestShutdownImpl(); s_session->requestInit(); return retval; } static String php_session_encode() { if (!PS(serializer)) { raise_warning("Unknown session.serialize_handler. " "Failed to encode session object"); return String(); } return PS(serializer)->encode(); } static void php_session_decode(CStrRef value) { if (!PS(serializer)) { raise_warning("Unknown session.serialize_handler. " "Failed to decode session object"); return; } if (!PS(serializer)->decode(value)) { php_session_destroy(); raise_warning("Failed to decode session object. " "Session has been destroyed"); } } static void php_session_initialize() { /* check session name for invalid characters */ if (strpbrk(PS(id).data(), "\r\n\t <>'\"\\")) { PS(id).reset(); } if (!PS(mod)) { raise_error("No storage module chosen - failed to initialize session"); return; } /* Open session handler first */ if (!PS(mod)->open(PS(save_path).c_str(), PS(session_name).c_str())) { raise_error("Failed to initialize storage module: %s (path: %s)", PS(mod)->getName(), PS(save_path).c_str()); return; } /* If there is no ID, use session module to create one */ int attempts = 3; if (PS(id).empty()) { new_session: PS(id) = PS(mod)->create_sid(); if (PS(id).empty()) { raise_error("Failed to create session id: %s", PS(mod)->getName()); return; } if (PS(use_cookies)) { PS(send_cookie) = 1; } } /* Read data */ /* Question: if you create a SID here, should you also try to read data? * I'm not sure, but while not doing so will remove one session operation * it could prove usefull for those sites which wish to have "default" * session information */ /* Unconditionally destroy existing arrays -- possible dirty data */ GlobalVariables *g = get_global_variables(); g->getRef(s__SESSION) = Array::Create(); PS(invalid_session_id) = false; String value; if (PS(mod)->read(PS(id).data(), value)) { php_session_decode(value); } else if (PS(invalid_session_id)) { /* address instances where the session read fails due to an invalid id */ PS(invalid_session_id) = false; PS(id).reset(); if (--attempts > 0) { goto new_session; } } } static void php_session_save_current_state() { bool ret = false; if (PS(mod)) { String value = php_session_encode(); if (!value.isNull()) { ret = PS(mod)->write(PS(id).data(), value); } } if (!ret) { raise_warning("Failed to write session data (%s). Please verify that the " "current setting of session.save_path is correct (%s)", PS(mod)->getName(), PS(save_path).c_str()); } if (PS(mod)) { PS(mod)->close(); } } /////////////////////////////////////////////////////////////////////////////// // Cookie Management #define COOKIE_SET_COOKIE "Set-Cookie: " #define COOKIE_EXPIRES "; expires=" #define COOKIE_PATH "; path=" #define COOKIE_DOMAIN "; domain=" #define COOKIE_SECURE "; secure" #define COOKIE_HTTPONLY "; HttpOnly" static void php_session_send_cookie() { Transport *transport = g_context->getTransport(); if (!transport) return; if (transport->headersSent()) { raise_warning("Cannot send session cookie - headers already sent"); return; } /* URL encode session_name and id because they might be user supplied */ String session_name = StringUtil::UrlEncode(String(PS(session_name).c_str())); String id = StringUtil::UrlEncode(PS(id)); StringBuffer ncookie; ncookie.append(COOKIE_SET_COOKIE); ncookie.append(session_name); ncookie.append('='); ncookie.append(id); if (PS(cookie_lifetime) > 0) { struct timeval tv; gettimeofday(&tv, NULL); time_t t = tv.tv_sec + PS(cookie_lifetime); if (t > 0) { ncookie.append(COOKIE_EXPIRES); ncookie.append(DateTime(t).toString(DateTime::DateFormat::Cookie)); } } if (!PS(cookie_path).empty()) { ncookie.append(COOKIE_PATH); ncookie.append(PS(cookie_path)); } if (!PS(cookie_domain).empty()) { ncookie.append(COOKIE_DOMAIN); ncookie.append(PS(cookie_domain)); } if (PS(cookie_secure)) { ncookie.append(COOKIE_SECURE); } if (PS(cookie_httponly)) { ncookie.append(COOKIE_HTTPONLY); } transport->addHeader(ncookie.detach()); } static void php_session_reset_id() { if (PS(use_cookies) && PS(send_cookie)) { php_session_send_cookie(); PS(send_cookie) = 0; } EnvConstants *g = get_env_constants(); if (PS(define_sid)) { StringBuffer var; var.append(String(PS(session_name))); var.append('='); var.append(PS(id)); g->k_SID = var.detach(); } else { g->k_SID = empty_string; } // hzhao: not sure how to support this yet #if 0 if (PS(apply_trans_sid)) { php_url_scanner_reset_vars(); php_url_scanner_add_var(PS(session_name), strlen(PS(session_name)), PS(id), strlen(PS(id)), 1); } #endif } /////////////////////////////////////////////////////////////////////////////// // Cache Limiters typedef struct { char *name; void (*func)(); } php_session_cache_limiter_t; #define CACHE_LIMITER(name) _php_cache_limiter_##name #define CACHE_LIMITER_FUNC(name) static void CACHE_LIMITER(name)() #define CACHE_LIMITER_ENTRY(name) { #name, CACHE_LIMITER(name) }, #define ADD_HEADER(hdr) g_context->getTransport()->addHeader(hdr) #define LAST_MODIFIED "Last-Modified: " #define EXPIRES "Expires: " #define MAX_STR 512 static char *month_names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static char *week_days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }; static inline void strcpy_gmt(char *ubuf, time_t *when) { char buf[MAX_STR]; struct tm tm, *res; int n; res = gmtime_r(when, &tm); if (!res) { buf[0] = '\0'; return; } n = snprintf(buf, sizeof(buf), "%s, %02d %s %d %02d:%02d:%02d GMT", // SAFE week_days[tm.tm_wday], tm.tm_mday, month_names[tm.tm_mon], tm.tm_year + 1900, tm.tm_hour, tm.tm_min, tm.tm_sec); memcpy(ubuf, buf, n); ubuf[n] = '\0'; } static const StaticString s_PATH_TRANSLATED("PATH_TRANSLATED"); static inline void last_modified() { GlobalVariables *g = get_global_variables(); String path = g->get(s__SERVER)[s_PATH_TRANSLATED].toString(); if (!path.empty()) { struct stat sb; if (stat(path.data(), &sb) == -1) { return; } char buf[MAX_STR + 1]; memcpy(buf, LAST_MODIFIED, sizeof(LAST_MODIFIED) - 1); strcpy_gmt(buf + sizeof(LAST_MODIFIED) - 1, &sb.st_mtime); ADD_HEADER(buf); } } CACHE_LIMITER_FUNC(public) { char buf[MAX_STR + 1]; struct timeval tv; time_t now; gettimeofday(&tv, NULL); now = tv.tv_sec + PS(cache_expire) * 60; memcpy(buf, EXPIRES, sizeof(EXPIRES) - 1); strcpy_gmt(buf + sizeof(EXPIRES) - 1, &now); ADD_HEADER(buf); snprintf(buf, sizeof(buf) , "Cache-Control: public, max-age=%" PRId64, PS(cache_expire) * 60); /* SAFE */ ADD_HEADER(buf); last_modified(); } CACHE_LIMITER_FUNC(private_no_expire) { char buf[MAX_STR + 1]; snprintf(buf, sizeof(buf), "Cache-Control: private, max-age=%" PRId64 ", " "pre-check=%" PRId64, PS(cache_expire) * 60, PS(cache_expire) * 60); /* SAFE */ ADD_HEADER(buf); last_modified(); } CACHE_LIMITER_FUNC(private) { ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT"); CACHE_LIMITER(private_no_expire)(); } CACHE_LIMITER_FUNC(nocache) { ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT"); /* For HTTP/1.1 conforming clients and the rest (MSIE 5) */ ADD_HEADER("Cache-Control: no-store, no-cache, must-revalidate, " "post-check=0, pre-check=0"); /* For HTTP/1.0 conforming clients */ ADD_HEADER("Pragma: no-cache"); } static php_session_cache_limiter_t php_session_cache_limiters[] = { CACHE_LIMITER_ENTRY(public) CACHE_LIMITER_ENTRY(private) CACHE_LIMITER_ENTRY(private_no_expire) CACHE_LIMITER_ENTRY(nocache) {0} }; static int php_session_cache_limiter() { if (PS(cache_limiter)[0] == '\0') return 0; Transport *transport = g_context->getTransport(); if (transport) { if (transport->headersSent()) { raise_warning("Cannot send session cache limiter - " "headers already sent"); return -2; } php_session_cache_limiter_t *lim; for (lim = php_session_cache_limiters; lim->name; lim++) { if (!strcasecmp(lim->name, PS(cache_limiter).c_str())) { lim->func(); return 0; } } } return -1; } /////////////////////////////////////////////////////////////////////////////// void f_session_set_cookie_params(int64_t lifetime, CStrRef path /* = null_string */, CStrRef domain /* = null_string */, CVarRef secure /* = null */, CVarRef httponly /* = null */) { if (PS(use_cookies)) { IniSetting::Set("session.cookie_lifetime", lifetime); if (!path.isNull()) { IniSetting::Set("session.cookie_path", path); } if (!domain.isNull()) { IniSetting::Set("session.cookie_domain", domain); } if (!secure.isNull()) { IniSetting::Set("session.cookie_secure", secure.toBoolean()); } if (!httponly.isNull()) { IniSetting::Set("session.cookie_httponly", httponly.toBoolean()); } } } static const StaticString s_lifetime("lifetime"); static const StaticString s_path("path"); static const StaticString s_domain("domain"); static const StaticString s_secure("secure"); static const StaticString s_httponly("httponly"); Array f_session_get_cookie_params() { ArrayInit ret(5); ret.set(s_lifetime, PS(cookie_lifetime)); ret.set(s_path, String(PS(cookie_path))); ret.set(s_domain, String(PS(cookie_domain))); ret.set(s_secure, PS(cookie_secure)); ret.set(s_httponly, PS(cookie_httponly)); return ret.create(); } String f_session_name(CStrRef newname /* = null_string */) { String oldname = String(PS(session_name)); if (!newname.isNull()) { IniSetting::Set("session.name", newname); } return oldname; } Variant f_session_module_name(CStrRef newname /* = null_string */) { String oldname; if (PS(mod) && PS(mod)->getName()) { oldname = String(PS(mod)->getName(), CopyString); } if (!newname.isNull()) { if (!SessionModule::Find(newname.data())) { raise_warning("Cannot find named PHP session module (%s)", newname.data()); return false; } if (PS(mod)) { PS(mod)->close(); } PS(mod) = NULL; IniSetting::Set("session.save_handler", newname); } return oldname; } bool f_hphp_session_set_save_handler(CObjRef sessionhandler, bool register_shutdown /* = true */) { if (PS(session_status) != Session::None) { return false; } PS(ps_session_handler) = sessionhandler; if (register_shutdown) { f_register_shutdown_function(1, String("session_write_close")); } IniSetting::Set("session.save_handler", "user"); return true; } String f_session_save_path(CStrRef newname /* = null_string */) { if (!newname.isNull()) { if (memchr(newname.data(), '\0', newname.size()) != NULL) { raise_warning("The save_path cannot contain NULL characters"); return false; } IniSetting::Set("session.save_path", newname); } return String(PS(save_path)); } String f_session_id(CStrRef newid /* = null_string */) { String ret = PS(id); if (!newid.isNull()) { PS(id) = newid; } return ret; } bool f_session_regenerate_id(bool delete_old_session /* = false */) { Transport *transport = g_context->getTransport(); if (transport && transport->headersSent()) { raise_warning("Cannot regenerate session id - headers already sent"); return false; } if (PS(session_status) == Session::Active) { if (!PS(id).empty()) { if (delete_old_session && !PS(mod)->destroy(PS(id).data())) { raise_warning("Session object destruction failed"); return false; } PS(id).reset(); } PS(id) = PS(mod)->create_sid(); PS(send_cookie) = 1; php_session_reset_id(); return true; } return false; } String f_session_cache_limiter(CStrRef new_cache_limiter /* = null_string */) { String ret(PS(cache_limiter)); if (!new_cache_limiter.isNull()) { IniSetting::Set("session.cache_limiter", new_cache_limiter); } return ret; } int64_t f_session_cache_expire(CStrRef new_cache_expire /* = null_string */) { int64_t ret = PS(cache_expire); if (!new_cache_expire.isNull()) { IniSetting::Set("session.cache_expire", new_cache_expire.toInt64()); } return ret; } Variant f_session_encode() { String ret = php_session_encode(); if (ret.isNull()) { return false; } return ret; } bool f_session_decode(CStrRef data) { if (PS(session_status) != Session::None) { php_session_decode(data); return true; } return false; } static const StaticString s_REQUEST_URI("REQUEST_URI"), s_HTTP_REFERER("HTTP_REFERER"); bool f_session_start() { PS(apply_trans_sid) = PS(use_trans_sid); String value; switch (PS(session_status)) { case Session::Active: raise_notice("A session had already been started - " "ignoring session_start()"); return false; case Session::Disabled: { if (!PS(mod) && IniSetting::Get("session.save_handler", value)) { PS(mod) = SessionModule::Find(value.data()); if (!PS(mod)) { raise_warning("Cannot find save handler '%s' - " "session startup failed", value.data()); return false; } } if (!PS(serializer) && IniSetting::Get("session.serialize_handler", value)) { PS(serializer) = SessionSerializer::Find(value.data()); if (!PS(serializer)) { raise_warning("Cannot find serialization handler '%s' - " "session startup failed", value.data()); return false; } } PS(session_status) = Session::None; /* fallthrough */ } default: assert(PS(session_status) == Session::None); PS(define_sid) = 1; PS(send_cookie) = 1; } /* * Cookies are preferred, because initially * cookie and get variables will be available. */ GlobalVariables *g = get_global_variables(); if (PS(id).empty()) { if (PS(use_cookies) && g->get(s__COOKIE).toArray().exists(String(PS(session_name)))) { PS(id) = g->get(s__COOKIE)[String(PS(session_name))].toString(); PS(apply_trans_sid) = 0; PS(send_cookie) = 0; PS(define_sid) = 0; } if (!PS(use_only_cookies) && !PS(id) && g->get(s__GET).toArray().exists(String(PS(session_name)))) { PS(id) = g->get(s__GET)[String(PS(session_name))].toString(); PS(send_cookie) = 0; } if (!PS(use_only_cookies) && !PS(id) && g->get(s__POST).toArray().exists(String(PS(session_name)))) { PS(id) = g->get(s__POST)[String(PS(session_name))].toString(); PS(send_cookie) = 0; } } int lensess = PS(session_name).size(); /* check the REQUEST_URI symbol for a string of the form '=' to allow URLs of the form http://yoursite/=/script.php */ if (!PS(use_only_cookies) && PS(id).empty()) { value = g->get(s__SERVER)[s_REQUEST_URI].toString(); const char *p = strstr(value.data(), PS(session_name).c_str()); if (p && p[lensess] == '=') { p += lensess + 1; const char *q; if ((q = strpbrk(p, "/?\\"))) { PS(id) = String(p, q - p, CopyString); PS(send_cookie) = 0; } } } /* check whether the current request was referred to by an external site which invalidates the previously found id */ if (!PS(id).empty() && PS(extern_referer_chk)[0] != '\0') { value = g->get(s__SERVER)[s_HTTP_REFERER].toString(); if (strstr(value.data(), PS(extern_referer_chk).c_str()) == NULL) { PS(id).reset(); PS(send_cookie) = 1; if (PS(use_trans_sid)) { PS(apply_trans_sid) = 1; } } } php_session_initialize(); if (!PS(use_cookies) && PS(send_cookie)) { if (PS(use_trans_sid)) { PS(apply_trans_sid) = 1; } PS(send_cookie) = 0; } php_session_reset_id(); PS(session_status) = Session::Active; php_session_cache_limiter(); if (PS(mod) && PS(gc_probability) > 0) { int nrdels = -1; int nrand = (int) ((float) PS(gc_divisor) * math_combined_lcg()); if (nrand < PS(gc_probability)) { PS(mod)->gc(PS(gc_maxlifetime), &nrdels); } } if (PS(session_status) != Session::Active) { return false; } return true; } bool f_session_destroy() { bool retval = true; if (PS(session_status) != Session::Active) { raise_warning("Trying to destroy uninitialized session"); return false; } if (!PS(mod)->destroy(PS(id).data())) { retval = false; raise_warning("Session object destruction failed"); } s_session->requestShutdownImpl(); s_session->requestInit(); return retval; } Variant f_session_unset() { if (PS(session_status) == Session::None) { return false; } GlobalVariables *g = get_global_variables(); g->getRef(s__SESSION).reset(); return uninit_null(); } void f_session_write_close() { if (PS(session_status) == Session::Active) { PS(session_status) = Session::None; php_session_save_current_state(); } } void f_session_commit() { f_session_write_close(); } bool f_session_register(int _argc, CVarRef var_names, CArrRef _argv /* = null_array */) { throw NotSupportedException (__func__, "Deprecated as of PHP 5.3.0 and REMOVED as of PHP 6.0.0. " "Relying on this feature is highly discouraged."); } bool f_session_unregister(CStrRef varname) { throw NotSupportedException (__func__, "Deprecated as of PHP 5.3.0 and REMOVED as of PHP 6.0.0. " "Relying on this feature is highly discouraged."); } bool f_session_is_registered(CStrRef varname) { throw NotSupportedException (__func__, "Deprecated as of PHP 5.3.0 and REMOVED as of PHP 6.0.0. " "Relying on this feature is highly discouraged."); } void c_SessionHandler::t___construct() { } c_SessionHandler::c_SessionHandler(Class* cb) : ExtObjectData(cb) { m_mod = PS(mod); } c_SessionHandler::~c_SessionHandler() { } bool c_SessionHandler::t_open(CStrRef save_path, CStrRef session_id) { return m_mod->open(save_path->data(), session_id->data()); } bool c_SessionHandler::t_close() { return m_mod->close(); } String c_SessionHandler::t_read(CStrRef session_id) { String value; if (m_mod->read(PS(id).data(), value)) { php_session_decode(value); return value; } return uninit_null(); } bool c_SessionHandler::t_write(CStrRef session_id, CStrRef session_data) { return m_mod->write(session_id->data(), session_data->data()); } bool c_SessionHandler::t_destroy(CStrRef session_id) { return m_mod->destroy(session_id->data()); } bool c_SessionHandler::t_gc(int maxlifetime) { int nrdels = -1; return m_mod->gc(maxlifetime, &nrdels); } /////////////////////////////////////////////////////////////////////////////// }