From: Kevin Day Date: Sat, 18 Feb 2017 23:00:09 +0000 (-0600) Subject: Update: add custom/experimental sessionize accounts and postgresql account auto-creat... X-Git-Url: https://git.kevux.org/?a=commitdiff_plain;h=4f5f271fed6341f9f38609c3de7f6139700c1133;p=koopa Update: add custom/experimental sessionize accounts and postgresql account auto-creation programs/scripts This adds some experimental helpers for either creating postgresql accounts via ldap or providing session management independent of PHP. Some of the C code is not using threads but likely should. I will eventually come back and make a threaded version. --- diff --git a/program/autocreate_ldap_accounts_in_postgresql/readme.txt b/program/autocreate_ldap_accounts_in_postgresql/readme.txt new file mode 100644 index 0000000..f7f08d4 --- /dev/null +++ b/program/autocreate_ldap_accounts_in_postgresql/readme.txt @@ -0,0 +1,23 @@ +Installation +============ +This assumes that the /programs/ paths are being used. + +Compile the source code: + gcc -g -lldap -lpq source/c/autocreate_ldap_accounts_in_postgresql.c -o /programs/bin/autocreate_ldap_accounts_in_postgresql + +Add and enable the init script: + cp -v source/bash/autocreate_ldap_accounts_in_postgresql.sh /etc/init.d/autocreate_ldap_accounts_in_postgresql + chkconfig --add autocreate_ldap_accounts_in_postgresql + chkconfig autocreate_ldap_accounts_in_postgresql on + +Configure the settings (assuming system called "example"): + mkdir -vp /programs/settings/autocreate_ldap_accounts_in_postgresql/ + cp -v settings/{example,systems}.settings /programs/settings/autocreate_ldap_accounts_in_postgresql/ + +Note: rename 'example.settings' to the name of the system as defined in 'systems.settings'. + +Start the service + service autocreate_ldap_accounts_in_postgresql start + +For most users, the /programs/ path needs to be changed to a custom path for your system. +The source code and bash scripts will need to be updated with these hardcoded paths. diff --git a/program/autocreate_ldap_accounts_in_postgresql/settings/example.settings b/program/autocreate_ldap_accounts_in_postgresql/settings/example.settings new file mode 100644 index 0000000..cdd685c --- /dev/null +++ b/program/autocreate_ldap_accounts_in_postgresql/settings/example.settings @@ -0,0 +1,6 @@ +# fss-0000 + +alap_name_system example +alap_name_group example_users +alap_name_database example_database +alap_port 1234 diff --git a/program/autocreate_ldap_accounts_in_postgresql/settings/systems.settings b/program/autocreate_ldap_accounts_in_postgresql/settings/systems.settings new file mode 100644 index 0000000..bcc2f9e --- /dev/null +++ b/program/autocreate_ldap_accounts_in_postgresql/settings/systems.settings @@ -0,0 +1,3 @@ +# fss-0000 + +alap_systems example diff --git a/program/autocreate_ldap_accounts_in_postgresql/source/bash/autocreate_ldap_accounts_in_postgresql.sh b/program/autocreate_ldap_accounts_in_postgresql/source/bash/autocreate_ldap_accounts_in_postgresql.sh new file mode 100644 index 0000000..a20217c --- /dev/null +++ b/program/autocreate_ldap_accounts_in_postgresql/source/bash/autocreate_ldap_accounts_in_postgresql.sh @@ -0,0 +1,471 @@ +#!/bin/bash +# +# autocreate_ldap_accounts_in_postgresql Helper service for auto-populating ldap accounts in postgresql. +# +# chkconfig: 345 40 60 +# description: Provide a per-database/per-role way to auto-create ldap accounts and auto assign a single role. + +### BEGIN INIT INFO +# Provides: autocreate_ldap_accounts_in_postgresql +# Required-Start: $local_fs $network +# Required-Stop: $local_fs $network +# Default-Start: 3 4 5 +# Default-Stop: 0 1 2 6 +# Short-Description: Auto-create ldap-based accounts in postgresql. +# Description: Provide a per-database/per-role way to auto-create ldap accounts and auto assign a single role. +### END INIT INFO + +# Source function library. +. /etc/rc.d/init.d/functions + +main() { + local process_owner="alap" + local process_group="alap" + local path_programs="/programs/" + local path_service="${path_programs}bin/autocreate_ldap_accounts_in_postgresql" + local path_settings="${path_programs}settings/autocreate_ldap_accounts_in_postgresql/" + local path_systems="${path_settings}systems.settings" + local path_pids="/var/run/autocreate_ldap_accounts_in_postgresql/" + local parameter_system=$2 + local alap_systems= + local i= + local j= + + # when process_owner is defined, make sure that the binary has the following set: + # setcap cap_net_bind_service=ep /programs/bin/autocreate_ldap_accounts_in_postgresql + + if [[ ! -f $path_systems ]] ; then + echo "No valid path_systems file defined at: $path_systems" + exit -1 + fi + + if [[ ! -d $path_pids ]] ; then + mkdir -p $path_pids + fi + + if [[ $process_owner != "" ]] ; then + chown $process_owner $path_pids + fi + + alap_systems=$(grep -o '^alap_systems[[:space:]][[:space:]]*.*$' $path_systems | sed -e 's|^alap_systems[[:space:]][[:space:]]*||') + + if [[ $alap_systems == "" ]] ; then + echo "No valid systems defined by setting 'alap_systems' in file: $path_systems" + exit -1 + fi + + if [[ $parameter_system != "" ]] ; then + j=$alap_systems + alap_systems= + + for i in $j ; do + if [[ $i == $parameter_system ]] ; then + alap_systems=$i + break; + fi + done + + i= + j= + + if [[ $alap_systems == "" ]] ; then + echo "System '$parameter_system' is not a valid system defined by setting 'alap_systems' in file: $path_systems" + exit -1 + fi + fi + + j=$alap_systems + alap_systems= + for i in $j ; do + if [[ -f $path_settings${i}.settings ]] ; then + alap_systems="$alap_systems$i " + else + echo "Skipping system '$i' because it does not have a settings file defined here: '$path_settings${i}.settings'" + fi + done + + i= + j= + + case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + status) + status + ;; + *) + echo "Usage: autocreate_ldap_accounts_in_postgresql {start|stop|restart|status}" + return 2 + esac + + return $? +} + +start() { + local alap_name_system= + local alap_name_group= + local alap_name_database= + local alap_connect_user= + local alap_connect_password= + local alap_port= + local alap_system= + local result= + local any_success=0 + local any_failure=0 + + for alap_system in $alap_systems ; do + load_system_settings + check_pid + + if [[ $result -eq -1 ]] ; then + continue + elif [[ $result -gt 0 ]] ; then + echo "Not starting process for $alap_system, it is already running with pid=$pid." + continue + fi + + start_command + + if [[ $result -eq 0 ]] ; then + wait_pid + + if [[ $result -eq 0 ]] ; then + get_pid + fi + + if [[ $pid == "" ]] ; then + echo "Started process for $alap_system but was unable to determine pid, command: $path_service $alap_name_system $alap_name_group $alap_name_database $alap_port." + else + echo "Successfully started process for $alap_system, pid=$pid, command: $path_service $alap_name_system $alap_name_group $alap_name_database $alap_port." + fi + fi + done + + if [[ $any_success -ne 0 || $any_failure -eq 1 ]] ; then + exit -1 + fi + + return 0 +} + +stop() { + local alap_name_system= + local alap_name_group= + local alap_name_database= + local alap_port= + local alap_system= + local result= + local any_success=0 + local any_failure=0 + local original_pid= + + for alap_system in $alap_systems ; do + load_system_settings + get_pid + + if [[ $pid == "" ]] ; then + continue + fi + + stop_command + + if [[ $result -eq 0 ]] ; then + original_pid=$pid + check_pid + + if [[ $result -eq -2 ]] ; then + echo "Successfully stopped process for $alap_system, pid=$original_pid." + else + echo "Sent stop command for $alap_system, pid=$pid, but pid file ($pid_file) still exists." + fi + fi + done + + if [[ $any_success -ne 0 || $any_failure -eq 1 ]] ; then + exit -1 + fi + + return 0 +} + +restart() { + local alap_name_system= + local alap_name_group= + local alap_name_database= + local alap_port= + local alap_system= + local result= + local any_success=0 + local any_failure=0 + local original_pid= + + for alap_system in $alap_systems ; do + load_system_settings + check_pid + + if [[ $result -lt 0 ]] ; then + continue + elif [[ $result -gt 0 ]] ; then + stop_command + + if [[ $result -ne 0 ]] ; then + continue + fi + + original_pid=$pid + check_pid + + if [[ $result -eq -2 ]] ; then + echo "Successfully stopped process for $alap_system, pid=$original_pid." + else + echo "Sent stop command for $alap_system, pid=$original_pid, but pid file ($pid_file) still exists (cannot start process, skipping)." + continue + fi + fi + + start_command + + if [[ $result -eq 0 ]] ; then + wait_pid + + if [[ $result -eq 0 ]] ; then + get_pid + fi + + if [[ $pid == "" ]] ; then + echo "Started process for $alap_system but was unable to determine pid, command: $path_service $alap_name_system $alap_name_group $alap_name_database $alap_port." + else + echo "Successfully started process for $alap_system, pid=$pid, command: $path_service $alap_name_system $alap_name_group $alap_name_database $alap_port." + fi + fi + done + + if [[ $any_success -ne 0 ]] ; then + exit -1 + fi + + return 0 +} + +status() { + local alap_name_system= + local alap_name_group= + local alap_name_database= + local alap_port= + local alap_system= + local pid_file= + local pid= + local result= + + for alap_system in $alap_systems ; do + load_system_settings + get_pid + + if [[ $pid == "" ]] ; then + continue + fi + + echo "The system '$alap_system' appears to be running as process $pid." + done + + return 0 +} + +load_system_settings() { + local path_system=$path_settings${alap_system}.settings + alap_name_system= + alap_name_group= + alap_name_database= + alap_connect_user= + alap_connect_password= + alap_port= + + if [[ $alap_system == "" || ! -f $path_system ]] ; then + echo "No valid path_systems file defined at: $path_system" + exit -1 + fi + + alap_name_system=$(grep -o '^alap_name_system[[:space:]][[:space:]]*.*$' $path_system | sed -e 's|^alap_name_system[[:space:]][[:space:]]*||') + alap_name_group=$(grep -o '^alap_name_group[[:space:]][[:space:]]*.*$' $path_system | sed -e 's|^alap_name_group[[:space:]][[:space:]]*||') + alap_name_database=$(grep -o '^alap_name_database[[:space:]][[:space:]]*.*$' $path_system | sed -e 's|^alap_name_database[[:space:]][[:space:]]*||') + alap_connect_user=$(grep -o '^alap_connect_user[[:space:]][[:space:]]*.*$' $path_system | sed -e 's|^alap_connect_user[[:space:]][[:space:]]*||') + alap_connect_password=$(grep -o '^alap_connect_password[[:space:]][[:space:]]*.*$' $path_system | sed -e 's|^alap_connect_password[[:space:]][[:space:]]*||') + alap_port=$(grep -o '^alap_port[[:space:]][[:space:]]*.*$' $path_system | sed -e 's|^alap_port[[:space:]][[:space:]]*||') + + if [[ $alap_name_system == "" ]] ; then + echo "No valid alap_name_system setting defined in file: $path_system" + exit -1 + fi + + if [[ $alap_name_group == "" ]] ; then + echo "No valid alap_name_group setting defined in file: $path_system" + exit -1 + fi + + if [[ $alap_name_database == "" ]] ; then + echo "No valid alap_name_database setting defined in file: $path_system" + exit -1 + fi + + if [[ $alap_connect_user == "" ]] ; then + echo "No valid alap_connect_user setting defined in file: $path_system" + exit -1 + fi + + if [[ $alap_port == "" ]] ; then + echo "No valid alap_port setting defined in file: $path_system" + exit -1 + fi +} + +start_command() { + export alap_connect_user="$alap_connect_user" + export alap_connect_password="$alap_connect_password" + + if [[ $process_owner == "" ]] ; then + $path_service $alap_name_system $alap_name_group $alap_name_database $alap_port + result=$? + else + su $process_owner -m -c "$path_service $alap_name_system $alap_name_group $alap_name_database $alap_port" + result=$? + fi + + if [[ $result -ne 0 ]] ; then + echo "Failed to start process, command: $path_service $alap_name_system $alap_name_group $alap_name_database $alap_port." + any_failure=1 + else + any_success=1 + fi +} + +stop_command() { + # -3 = SIGQUIT, -15 = SIGTERM, -9 = SIGKILL + kill -3 $pid + result=$? + + if [[ $result -ne 0 ]] ; then + echo "Signal to quit failed, command: kill -3 $pid." + any_failure=1 + else + any_success=1 + + # pause and give the process time to close down. + sleep 0.1 + fi +} + +wait_pid() { + local k= + local max=32 + + pid= + pid_file=$path_pids$alap_system.pid + result=-1 + + # the started process will go into the background, so wait until the pid file is created, but only wait for so long. + let k=0 + while [[ $k -lt $max ]] ; do + if [[ -f $pid_file ]] ; then + result=0 + break + fi + + sleep 0.05 + + let k=$k+1 + done + + return 0 +} + +get_pid() { + pid= + pid_file=$path_pids$alap_system.pid + + if [[ ! -f $pid_file ]] ; then + echo "No pid file ($pid_file) found for system '$alap_system', it must not be running." + return 0 + fi + + pid=$(cat $pid_file) + result=$? + + if [[ $result -ne 0 ]] ; then + echo "Failed to read the pid file ($pid_file) for system '$alap_system', command: cat $pid_file." + pid= + return 0 + fi + + if [[ $pid == "" ]] ; then + echo "The pid file ($pid_file) for system '$alap_system' is empty." + pid= + return 0 + fi + + result=$(ps --no-headers -o pid -p $pid) + if [[ $? -lt 0 ]] ; then + echo "An error occured while searching for the process for system '$alap_system', command: ps --no-headers -o pid -p $pid." + pid= + return 0 + fi + + if [[ $result == "" ]] ; then + echo "No process $pid was found for the system '$alap_system', the pid file might be stale or inaccurate." + pid= + return 0 + fi +} + +check_pid() { + pid= + pid_file=$path_pids$alap_system.pid + + if [[ ! -f $pid_file ]] ; then + result=-2 + return 0 + fi + + pid=$(cat $pid_file) + result=$? + + if [[ $result -ne 0 ]] ; then + echo "Failed to read the pid file ($pid_file) for system '$alap_system', command: cat $pid_file." + result=-1 + return 0 + fi + + if [[ $pid == "" ]] ; then + result=0 + return 0 + fi + + result=$(ps --no-headers -o pid -p $pid) + if [[ $? -lt 0 ]] ; then + echo "An error occured while searching for the process for system '$alap_system', command: ps --no-headers -o pid -p $pid." + result=-1 + return 0 + fi + + if [[ $result == "" ]] ; then + result= + + # the pid file is invalid, so remove the pid file. + rm -f $pid_file + + # return 0 to allow for starting a new process. + result=0 + return 0 + fi + + result=1 + return 0 +} + +main "$1" "$2" diff --git a/program/autocreate_ldap_accounts_in_postgresql/source/c/autocreate_ldap_accounts_in_postgresql.c b/program/autocreate_ldap_accounts_in_postgresql/source/c/autocreate_ldap_accounts_in_postgresql.c new file mode 100644 index 0000000..8767105 --- /dev/null +++ b/program/autocreate_ldap_accounts_in_postgresql/source/c/autocreate_ldap_accounts_in_postgresql.c @@ -0,0 +1,1551 @@ +/** + * Helper program for auto-creating ldap accounts. + * + * This was written originally using sockets, but it makes more sense to run this on the database server (for security reasons). + * - The original socket code is left alone, but is not used. + * + * The program expects the following parameters: [user_name] [group_name] [database_name] [listen_port]. + * + * The system will listen on the socket waiting on a valid username to create. + * This only accept usernames with alphanumeric, '-', or '_' in their name. + * - All other characters will result in an error. + * - Some username patterns will be blacklisted. (@todo: implement this via regex.) + * + * A packet size of PACKET_SIZE_INPUT is defined to ensure that the string is operated on only after all data is received. + * - A NULL byte before the PACKET_SIZE_INPUT is reached will also terminate the packet. + * + * @todo: review this functionality "http://www.postgresql.org/docs/current/static/libpq-notice-processing.html". + * + * Compiled with: + * gcc -lpq -lldap autocreate_ldap_accounts_in_postgresql.c -o autocreate_ldap_accounts_in_postgresql + * + * Role created with: + * create role create_ldap_users createrole; + * alter role create_ldap_users login; + * grant fcs_users to create_ldap_users with admin option; + * + * Alternatively, roles can be granted CREATEROLE, such as: + * grant createrole to create_ldap_users + * + * @todo: when implementing the init script, the pid can be created via: ps --no-headers -o pid -p $(cat fcs.pid) + * + * Copyright Kevin Day, lgpl v2.1 or later. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +// select either socket or network method. +//#define USE_SOCKET 1 +#define USE_NETWORK 1 +//#define DEBUG_ENABLED 1 + +#define LOG_ID "autocreate_ldap_accounts_in_postgresql: " +#define PATH_PID "/var/run/autocreate_ldap_accounts_in_postgresql/%s.pid" + +// by granting a postgresql user the same access as a specified role, one can easily manage access by only setting permissions on the role. +// for consistency purposes, I suggest individual users have something like 'fcs_user' while the role/group should be something like 'fcs_users'. +// with this design admin users need to be manually updated on the database to have access to create users as well. +// admin users would then need something like this: "grant fcs_users to kday with admin option;". +#define PSQL_SELECT "select rolname from pg_roles where rolname = '%s';" +#define PSQL_SELECT_LENGTH 48 +#define PSQL_CREATE "create role %s with login inherit;" +#define PSQL_CREATE_LENGTH 32 +#define PSQL_GRANT "grant %s to %s;" +#define PSQL_GRANT_LENGTH 11 +//#define PSQL_CONNECTION "host=127.0.0.1 port=5433 dbname=%s connect_timeout=2 sslmode=require user= password=" +#define PSQL_CONNECTION "port=5433 dbname=%s connect_timeout=2 sslmode=disable user=%s password=%s" +#define PSQL_CONNECTION_LENGTH 73 + +#define PARAMETER_LENGTH_MAX 96 + +#define LDAP_SERVER "ldaps://ldap.example.com:1636" +#define LDAP_SEARCH_DN "uid=%s,ou=users,ou=People" +#define LDAP_SEARCH_DN_LENGTH 47 + +#define LDAP_RETRY_BIND_RETRY 4 +#define LDAP_RETRY_BIND_TIMEOUT 200000 // (microseconds) 0.2 second timeout. +#define LDAP_RETRY_SEARCH_RETRY 4 +#define LDAP_RETRY_SEARCH_TIMEOUT 200000 // (microseconds) 0.2 second timeout. + +#define PACKET_SIZE_INPUT 63 +#define PACKET_SIZE_OUTPUT 1 + +// if stack size is too small, then on some systems (generally glibc based ones) will segfault/illegal-instruction under certain circumstances. +// in this case it the circumstance happens with clone(), ldap_initialize(), and glibc. +//#define STACK_SIZE 8192 +#define STACK_SIZE 65536 + +#define PROTOCOL_NULL 0 +#define PROTOCOL_SOCKET SOL_SOCKET +#define PROTOCOL_TCP 6 +#define PROTOCOL_UDP 17 + +#define SOCKET_TYPE SOCK_STREAM +#define SOCKET_BACKLOG 15 + +#ifdef USE_NETWORK + #define SOCKET_FAMILY AF_INET // 'family' is also called 'domain' in this case. + #define SOCKET_PROTOCOL PROTOCOL_TCP + #define SOCKET_TIMEOUT 160000 // 0.16 seconds. +#elif defined USE_SOCKET + #define SOCKET_FAMILY AF_UNIX // 'family' is also called 'domain' in this case. + #define SOCKET_PATH "/var/www/sockets/autocreate_ldap_accounts_in_postgresql/%s/%s.socket" + #define SOCKET_PATH_LENGTH 64 + #define SOCKET_PROTOCOL PROTOCOL_NULL + #define SOCKET_PORT 0 + #define SOCKET_TIMEOUT 10000 // (microseconds) 0.01 seconds. +#endif // USE_SOCKET + +#define FLAGS_RECEIVE 0 +#define FLAGS_SEND MSG_NOSIGNAL +#define FLAGS_CLONE CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_VM | CLONE_SIGHAND | CLONE_THREAD + + +// environment variables used. +#define ENVIRONMENT_CONNECT_USER "alap_connect_user" +#define ENVIRONMENT_CONNECT_PASSWORD "alap_connect_password" + +#define ENVIRONMENT_MAX_CONNECT_USER 128 // maximum characters to be supported for the connect name. +#define ENVIRONMENT_MAX_CONNECT_PASSWORD 512 // maximum characters to be supported for the connect password. + + +// note: these are strings instead of integers so that they act as binary data when passed as a string via the socket. +#define ERROR_NONE "\x00" +#define ERROR_NAME "\x01" // invalid user name, bad characters, or name too long. +#define ERROR_LDAP "\x02" // failed to connect to the ldap server and could not query the ldap name. +#define ERROR_USER "\x03" // user name not found in ldap database. +#define ERROR_DATABASE "\x04" // failed to connect to the database. +#define ERROR_SQL "\x05" // error returned while executing the SQL command. +#define ERROR_READ "\x06" // error occured while reading input from the user (such as via recv()). +#define ERROR_WRITE "\x07" // error occured while writing input from the user (such as via send()). +#define ERROR_PACKET "\x08" // the received packet is invalid, such as wrong length. +#define ERROR_TIMEOUT "\x09" // connection timed out when reading or writing. +#define ERROR_CLOSE "\x0a" // the connection is being forced closed. +#define ERROR_QUIT "\x0b" // the connection is closing because the service is quitting. + +#define PROBLEM_COUNT_MAX_SIGNAL_SIZE 10 + +#ifdef USE_NETWORK + #define MACRO_EXIT_STANDARD_1(shared, stack, exit_code) \ + if (shared.socket_id_client > 0) { \ + send(shared.socket_id_client, ERROR_QUIT, PACKET_SIZE_OUTPUT, FLAGS_SEND); \ + shutdown(shared.socket_id_client, SHUT_RDWR); \ + } \ + \ + if (shared.socket_id_target > 0) { \ + shutdown(shared.socket_id_target, SHUT_RDWR); \ + } \ + \ + if (shared.socket_bound > 0) { \ + shared.socket_bound = 0; \ + } \ + \ + if (stack != NULL) { \ + free(stack); \ + stack = NULL; \ + } \ + \ + if (shared.pid_path != NULL) { \ + unlink(shared.pid_path); \ + free(shared.pid_path); \ + shared.pid_path = NULL; \ + } \ + \ + memset(&shared, 0, sizeof(shared_data)); \ + \ + return exit_code; + + typedef struct { + char parameter_system[PARAMETER_LENGTH_MAX]; + char parameter_group[PARAMETER_LENGTH_MAX]; + char parameter_database[PARAMETER_LENGTH_MAX]; + char parameter_connect_name[ENVIRONMENT_MAX_CONNECT_USER]; + char parameter_connect_password[ENVIRONMENT_MAX_CONNECT_PASSWORD]; + + int socket_id_target; + int socket_id_client; + int socket_bound; + + int parameter_port; + + pid_t pid_parent; + pid_t pid_child; + char *pid_path; + } shared_data; +#elif defined USE_SOCKET + #define MACRO_EXIT_STANDARD_1(shared, stack, exit_code) \ + if (shared.socket_id_client > 0) { \ + send(shared.socket_id_client, ERROR_QUIT, PACKET_SIZE_OUTPUT, FLAGS_SEND); \ + shutdown(shared.socket_id_client, SHUT_RDWR); \ + } \ + \ + if (shared.socket_id_target > 0) { \ + shutdown(shared.socket_id_target, SHUT_RDWR); \ + } \ + \ + if (shared.socket_bound > 0) { \ + shared.socket_bound = 0; \ + } \ + \ + if (stack != NULL) { \ + free(stack); \ + stack = NULL; \ + } \ + \ + if (shared.socket_path != NULL) { \ + unlink(shared.socket_path); \ + } \ + \ + if (shared.pid_path != NULL) { \ + unlink(shared.pid_path); \ + free(shared.pid_path); \ + shared.pid_path = NULL; \ + } \ + \ + memset(&shared, 0, sizeof(shared_data)); \ + \ + return exit_code; + + typedef struct { + char parameter_system[PARAMETER_LENGTH_MAX]; + char parameter_group[PARAMETER_LENGTH_MAX]; + char parameter_database[PARAMETER_LENGTH_MAX]; + char parameter_connect_name[ENVIRONMENT_MAX_CONNECT_USER]; + char parameter_connect_password[ENVIRONMENT_MAX_CONNECT_PASSWORD]; + + int socket_id_target; + int socket_id_client; + int socket_bound; + + char *socket_path; + + pid_t pid_parent; + pid_t pid_child; + char pid_path[PATH_MAX]; + } shared_data; +#endif // USE_SOCKET + +#define MACRO_EXIT_STANDARD_2(pid_child, shared, stack, exit_code) \ + if (pid_child > 0) { \ + kill(pid_child, SIGQUIT); \ + pid_child = 0; \ + } \ + \ + MACRO_EXIT_STANDARD_1(shared, stack, exit_code) + + +/** + * Immediately writes to system logger. + * + * @param const unsigned level + * The log messages level. + * LOG_ERR is the most common choice here. + * This is not the 'priority', which is auto-generated. + * + * @param const char *message + * The complete message string to write to the logger. + * + * @see vsyslog() + * @see printf() + */ +void log_write(const int level, const char *message, ...) { + va_list arguments; + + #ifdef DEBUG_ENABLED + va_start(arguments, message); + vprintf(message, arguments); + fflush(stdout); + va_end(arguments); + #endif // DEBUG_ENABLED + + va_start(arguments, message); + + openlog(LOG_ID, LOG_PID | LOG_CONS, LOG_DAEMON); + vsyslog(level | LOG_DAEMON, message, arguments); + closelog(); + + va_end(arguments); +} + +/** + * Grants the user access to the specified group in the postgresql database. + * + * @param char *user_name + * Name of the user/role to grant access to.. + * @param char *user_password + * Password for the database user. + * @param char *group_name + * Name of the group. + * @param char *database_name + * Name of the database. + * @param char *connect_name + * Name of the role used to connect to the database. + * @param char *connect_password + * Password for the role used to connect to the database. + * + * @return int + * 1 on success and -1 on error. + */ +int grant_role_in_database(const char *user_name, const char *group_name, const char *database_name, const char *connect_name, const char *connect_password) { + PGconn *connection = NULL; + char *connection_information = NULL; + + { + int connection_information_length = PSQL_CONNECTION_LENGTH + 1; + + connection_information_length += strnlen(database_name, PARAMETER_LENGTH_MAX); + connection_information_length += strnlen(connect_name, PARAMETER_LENGTH_MAX); + connection_information_length += strnlen(connect_password, PARAMETER_LENGTH_MAX); + connection_information = malloc(sizeof(char) * connection_information_length); + + if (connection_information == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate memory when building the postgresql connection information string while processing user '%s', group '%s', and database '%s'.\n", user_name, group_name, database_name); + return -1; + } + + memset(connection_information, 0, sizeof(char) * connection_information_length); + snprintf(connection_information, connection_information_length, PSQL_CONNECTION, database_name, connect_name, connect_password); + } + + connection = PQconnectdb(connection_information); + + if (connection == NULL) { + log_write(LOG_ERR, "ERROR: failed to establish the postgresql connection while processing user '%s', group '%s', and database '%s', reason: NULL returned.\n", user_name, group_name, database_name); + return -1; + } + else if (PQstatus(connection) != CONNECTION_OK) { + log_write(LOG_ERR, "ERROR: failed to establish the postgresql connection while processing user '%s', group '%s', and database '%s', reason (%u): %s.\n", user_name, group_name, database_name, PQstatus(connection), PQerrorMessage(connection)); + PQfinish(connection); + free(connection_information); + connection_information = NULL; + return -1; + } + + // check to see if role exists and has + short role_exists = 0; + { + char *query = NULL; + int query_length = PSQL_SELECT_LENGTH + 1; + + query_length += strnlen(user_name, PARAMETER_LENGTH_MAX); + query = malloc(sizeof(char) * query_length); + + if (query == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate memory when building the postgresql select query string while processing user '%s', group '%s', and database '%s'.\n", user_name, group_name, database_name); + + PQfinish(connection); + free(connection_information); + connection_information = NULL; + return -1; + } + + memset(query, 0, sizeof(char) * query_length); + snprintf(query, query_length, PSQL_SELECT, user_name); + + { + PGresult *result = NULL; + + result = PQexec(connection, query); + + int status = PQresultStatus(result); + + if (status == PGRES_EMPTY_QUERY) { + //role_exists = 0; + } + else if (status == PGRES_COMMAND_OK || status == PGRES_TUPLES_OK) { + if (PQnfields(result) > 0 && PQntuples(result) > 0) { + role_exists = 1; + } + } + else { + PQclear(result); + PQfinish(connection); + + free(query); + free(connection_information); + + result = NULL; + query = NULL; + connection_information = NULL; + + return -1; + } + + PQclear(result); + result = NULL; + } + + free(query); + query = NULL; + } + + // Create the specified role. + if (role_exists == 0) { + char *query = NULL; + int query_length = PSQL_CREATE_LENGTH + 1; + + query_length += strnlen(user_name, PARAMETER_LENGTH_MAX); + query = malloc(sizeof(char) * query_length); + + if (query == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate memory when building the postgresql creates query string while processing user '%s', group '%s', and database '%s'.\n", user_name, group_name, database_name); + + PQfinish(connection); + free(connection_information); + connection_information = NULL; + return -1; + } + + memset(query, 0, sizeof(char) * query_length); + snprintf(query, query_length, PSQL_CREATE, user_name); + + { + PGresult *result = NULL; + + result = PQexec(connection, query); + + int status = PQresultStatus(result); + + if (status != PGRES_EMPTY_QUERY && status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) { + log_write(LOG_ERR, "ERROR: failed to process sql query '%s', reason (%u): %s.\n", query, status, PQerrorMessage(connection)); + + PQclear(result); + PQfinish(connection); + + free(query); + free(connection_information); + + result = NULL; + query = NULL; + connection_information = NULL; + + return -1; + } + + PQclear(result); + result = NULL; + } + + free(query); + query = NULL; + } + + // grant the user access to the specified role. + { + char *query = NULL; + int query_length = PSQL_GRANT_LENGTH + 1; + + query_length += strnlen(group_name, PARAMETER_LENGTH_MAX); + query_length += strnlen(user_name, PARAMETER_LENGTH_MAX); + query = malloc(sizeof(char) * query_length); + + if (query == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate memory when building the postgresql grant query string while processing user '%s', group '%s', and database '%s'.\n", user_name, group_name, database_name); + + PQfinish(connection); + free(connection_information); + connection_information = NULL; + return -1; + } + + memset(query, 0, sizeof(char) * query_length); + snprintf(query, query_length, PSQL_GRANT, group_name, user_name); + + { + PGresult *result = NULL; + + result = PQexec(connection, query); + + int status = PQresultStatus(result); + + if (status != PGRES_EMPTY_QUERY && status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) { + log_write(LOG_ERR, "ERROR: failed to process sql query '%s', reason (%u): %s.\n", query, status, PQerrorMessage(connection)); + + PQclear(result); + PQfinish(connection); + + free(query); + free(connection_information); + + result = NULL; + query = NULL; + connection_information = NULL; + + return -1; + } + + PQclear(result); + result = NULL; + } + + free(query); + query = NULL; + } + + if (connection_information != NULL) { + PQfinish(connection); + free(connection_information); + } + + return 1; +} + +/** + * Queries the name in the ldap server to see if it exists. + * + * @param const char *user_name + * The user name to query in the ldap database. + * + * @return bool + * 1 on found, 0 on not found, and -1 on error. + */ +int does_name_exist_in_ldap(const char *user_name) { + int user_name_length = 0; + int ldap_name_length = 0; + int ldap_status = 0; + LDAP *ldap_settings = NULL; + char *ldap_name = NULL; + + user_name_length = strnlen(user_name, PACKET_SIZE_INPUT); + ldap_name_length = user_name_length + LDAP_SEARCH_DN_LENGTH; + + ldap_name = malloc(sizeof(char) * (ldap_name_length + 1)); + if (ldap_name == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate memory when building the ldap user name for the user: '%s'.\n", user_name); + return -1; + } + + memset(ldap_name, 0, sizeof(char) * (ldap_name_length + 1)); + sprintf(ldap_name, LDAP_SEARCH_DN, user_name); + + + ldap_status = ldap_initialize(&ldap_settings, LDAP_SERVER); + + if (ldap_status != LDAP_SUCCESS) { + log_write(LOG_ERR, "ERROR: failed to initialize ldap settings for the ldap server '%s' with the ldap name '%s' with the ldap error (%d): %s.\n", LDAP_SERVER, ldap_name, ldap_status, ldap_err2string(ldap_status)); + + if (ldap_name != NULL) { + free(ldap_name); + } + return -1; + } + + + // a bind is ldap's way of saying 'login' or 'authenticate', do no use string to search with bind. + { + int tries = 0; + for (; tries < LDAP_RETRY_BIND_RETRY; tries++) { + ldap_status = ldap_simple_bind_s(ldap_settings, "", ""); + + if (ldap_status == LDAP_SUCCESS) { + break; + } + else if (ldap_status == LDAP_SERVER_DOWN) { + if (tries + 1 < LDAP_RETRY_BIND_RETRY) { + continue; + } + } + else if (ldap_status == LDAP_TIMEOUT) { + if (tries + 1 < LDAP_RETRY_BIND_RETRY) { + continue; + } + } + + log_write(LOG_ERR, "ERROR: failed to connect and bind to the ldap server '%s' with the ldap name '%s' with the ldap error (%d): %s\n", LDAP_SERVER, ldap_name, ldap_status, ldap_err2string(ldap_status)); + + if (ldap_name != NULL) { + free(ldap_name); + } + return -1; + } + } + + + // once bound, perform the search. + { + struct timeval ldap_timeout; + int ldap_sizelimit = 1; + int ldap_message_type = 0; + LDAPMessage *ldap_message = NULL; + int ldap_matched = 0; + + memset(&ldap_timeout, 0, sizeof(struct timeval)); + ldap_timeout.tv_sec = 0; + ldap_timeout.tv_usec = LDAP_RETRY_SEARCH_TIMEOUT; + + { + int tries = 0; + for (; tries < LDAP_RETRY_SEARCH_RETRY; tries++) { + ldap_status = ldap_search_ext_s(ldap_settings, ldap_name, LDAP_SCOPE_BASE, NULL, NULL, 0, NULL, NULL, &ldap_timeout, ldap_sizelimit, &ldap_message); + + ldap_message_type = ldap_msgtype(ldap_message); + + if (ldap_status == LDAP_SUCCESS) { + ldap_matched = ldap_count_entries(ldap_settings, ldap_message); + + // From manpage: "Note that res parameter of ldap_search_ext_s() and ldap_search_s() should be freed with ldap_msgfree() regardless of return value of these functions" + ldap_msgfree(ldap_message); + + break; + } + + // From manpage: "Note that res parameter of ldap_search_ext_s() and ldap_search_s() should be freed with ldap_msgfree() regardless of return value of these functions" + ldap_msgfree(ldap_message); + + if (ldap_status == LDAP_SERVER_DOWN) { + if (tries + 1 < LDAP_RETRY_SEARCH_RETRY) { + continue; + } + } + else if (ldap_status == LDAP_TIMEOUT) { + if (tries + 1 < LDAP_RETRY_SEARCH_RETRY) { + continue; + } + } + + log_write(LOG_ERR, "ERROR: failed to find '%s' on the ldap server '%s' with the ldap name '%s' with the ldap error (%d): %s\n", user_name, LDAP_SERVER, ldap_name, ldap_status, ldap_err2string(ldap_status)); + + if (ldap_settings != NULL) { + ldap_unbind(ldap_settings); + } + + if (ldap_name != NULL) { + free(ldap_name); + } + return -1; + } + } + + // Ldap is no longer needed. + if (ldap_settings != NULL) { + ldap_unbind(ldap_settings); + } + + if (ldap_name != NULL) { + free(ldap_name); + } + + if (ldap_matched == 0) { + return 0; + } + } + + return 1; +} + +/** + * Handles network connections. + * + * This is called by clone(). + * + * @param void *argument + * The data shared between the parent and cloned child. + * + * @see: clone() + */ +int handler_child(void *argument) { + // do no accept/allow signals in the child handler. + sigset_t signal_mask; + sigemptyset(&signal_mask); + sigprocmask(SIG_BLOCK, &signal_mask, NULL); + + shared_data *shared; + shared = (shared_data *) argument; + + //shared->pid_child = syscall(SYS_gettid); + shared->pid_child = getpid(); + + log_write(LOG_DEBUG, "DEBUG: after clone (child) pid = %u, child pid = %u, target socket id = %u\n", shared->pid_parent, shared->pid_child, shared->socket_id_target); + + const unsigned structure_socket_length = sizeof(struct sockaddr_un); + + #ifdef USE_NETWORK + { + // bind the socket to port. + struct addrinfo *port_information = NULL; + struct addrinfo port_setup; + + memset(&port_setup, 0, sizeof(struct addrinfo)); + + port_setup.ai_family = INADDR_ANY; + port_setup.ai_socktype = SOCKET_TYPE; + port_setup.ai_flags = AI_PASSIVE; + + { + char string_port[16]; + memset(&string_port, 0, sizeof(char) * 16); + + sprintf(string_port, "%u", shared->parameter_port); + + int addressed = getaddrinfo(NULL, string_port, &port_setup, &port_information); + if (addressed != 0) { + log_write(LOG_ERR, "ERROR: failed to process the port '%u' using protocol '%u', 'socket id = '%i': error %i (%u).\n", shared->parameter_port, SOCKET_PROTOCOL, shared->pid_child, addressed, errno); + + freeaddrinfo(port_information); + port_information = NULL; + + // send SIGCHLD signal to parent process. + if (shared->pid_parent > 0) { + kill(shared->pid_parent, SIGCHLD); + } + return -1; + } + } + + if (shared->socket_bound == 0) { + shared->socket_bound = bind(shared->socket_id_target, port_information->ai_addr, port_information->ai_addrlen); + if (shared->socket_bound < 0) { + log_write(LOG_ERR, "ERROR: failed to bind the port '%u' using protocol '%u', 'socket id = '%i': error %i (%u).\n", shared->parameter_port, SOCKET_PROTOCOL, shared->pid_child, shared->socket_bound, errno); + shared->socket_bound = 0; + + freeaddrinfo(port_information); + port_information = NULL; + + // send SIGQUIT signal to parent process. + if (shared->pid_parent > 0) { + kill(shared->pid_parent, SIGQUIT); + } + return -1; + } + + shared->socket_bound = 1; + } + + freeaddrinfo(port_information); + port_information = NULL; + } + #elif defined USE_SOCKET + { + // bind the socket to the shared.socket_path so that the + struct sockaddr_un socket_address; + + memset(&socket_address, 0, structure_socket_length); + socket_address.sun_family = SOCKET_FAMILY; + strncpy(socket_address.sun_path, shared->socket_path, sizeof(socket_address.sun_path) - 1); + + { + int bound = bind(shared->socket_id_target, (struct sockaddr *) &socket_address, structure_socket_length); + if (bound < 0) { + log_write(LOG_ERR, "ERROR: failed to bind the socket '%s' using protocol '%u', 'socket id = '%i'\n", shared->socket_path, SOCKET_PROTOCOL, shared->socket_id_target); + + // send SIGQUIT signal to parent process. + if (shared->pid_parent > 0) { + kill(shared->pid_parent, SIGQUIT); + } + return -1; + } + } + } + #endif // USE_SOCKET + + { + int listening = listen(shared->socket_id_target, SOCKET_BACKLOG); + + if (listening < 0) { + #ifdef USE_NETWORK + log_write(LOG_ERR, "ERROR: failed to listen to the port '%u' using protocol '%u', 'socket id = '%i', error %i (%u).\n", shared->parameter_port, SOCKET_PROTOCOL, shared->pid_child, listening, errno); + #elif defined USE_SOCKET + log_write(LOG_ERR, "ERROR: failed to listen to the socket '%s' using protocol '%u', 'socket id = '%i', error %i (%u).\n", shared->socket_path, SOCKET_PROTOCOL, shared->pid_child, listening, errno); + #endif // USE_SOCKET + + // send SIGQUIT signal to parent process. + if (shared->pid_parent > 0) { + kill(shared->pid_parent, SIGQUIT); + } + return -1; + } + } + + int socket_error = 0; + socklen_t socket_error_length = 0; + + int message_length = 0; + int i = 0; + int processed = 0; + + socklen_t length = 0; + ssize_t sent = 0; + + char buffer[PACKET_SIZE_INPUT]; + char user_name[PACKET_SIZE_INPUT]; + char *error_receive = ERROR_NONE; + + struct sockaddr_un socket_client_address; + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = SOCKET_TIMEOUT; + + while (1) { + length = structure_socket_length; + processed = 0; + error_receive = ERROR_NONE; + + // make sure that socket_id_client is always closed before continuing. + if (shared->socket_id_client != 0) { + sent = send(shared->socket_id_client, ERROR_CLOSE, PACKET_SIZE_OUTPUT, FLAGS_SEND); + close(shared->socket_id_client); + shared->socket_id_client = 0; + } + + memset(&socket_client_address, 0, structure_socket_length); + + shared->socket_id_client = accept(shared->socket_id_target, (struct sockaddr *) &socket_client_address, &length); + + if (shared->socket_id_client < 0) { + #ifdef USE_NETWORK + log_write(LOG_ERR, "ERROR: failed to accept connections on the port '%u' using protocol '%u': error %i (%u).\n", shared->parameter_port, SOCKET_PROTOCOL, shared->socket_id_client, errno); + #elif defined USE_SOCKET + log_write(LOG_ERR, "ERROR: failed to accept connections on the socket '%s' using protocol '%u': error %i (%u).\n", shared->socket_path, SOCKET_PROTOCOL, shared->socket_id_client, errno); + #endif // USE_SOCKET + + shared->socket_id_client = 0; + + // send SIGQUIT signal to parent process. + if (shared->pid_parent > 0) { + kill(shared->pid_parent, SIGQUIT); + } + + return -1; + } + + memset(&buffer, 0, sizeof(char) * PACKET_SIZE_INPUT); + memset(&user_name, 0, sizeof(char) * PACKET_SIZE_INPUT); + + // define a very short timeout for fast responses (both send and receive). + // the socket is expected to be a local connection, so it should be very fast. + setsockopt(shared->socket_id_client, PROTOCOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)); + setsockopt(shared->socket_id_client, PROTOCOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)); + + // linger connection for at most 2 seconds to help properly close connections. + { + struct linger linger_value = {1, 2}; + setsockopt(shared->socket_id_target, PROTOCOL_SOCKET, SO_LINGER, &linger_value, sizeof(struct linger)); + } + + // receive the user name from the client. + do { + error_receive = ERROR_NONE; + message_length = recv(shared->socket_id_client, buffer, PACKET_SIZE_INPUT, FLAGS_RECEIVE); + + if (message_length == 0) { + // this happens on proper client connection termination. + close(shared->socket_id_client); + shared->socket_id_client = 0; + break; + } + else if (message_length < 0) { + // this is not working as expected, socket_error is 0 when something like EAGAIN is expected. + // the idea here is to detect a timeout and report ERROR_TIMEOUT instead of ERROR_READ. + // until this is figured out, assume that ERROR_READ is the same as ERROR_TIMEOUT. + // look into recvmsg(). + //socket_error = 0; + //socket_error_length = sizeof(int); + //getsockopt(shared->socket_id_client, PROTOCOL_SOCKET, SO_ERROR, (void *) &socket_error, &socket_error_length); + send(shared->socket_id_client, ERROR_READ, PACKET_SIZE_OUTPUT, FLAGS_SEND); + break; + } + + // require a valid packet length. + if (message_length > PACKET_SIZE_INPUT) { + sent = send(shared->socket_id_client, ERROR_PACKET, PACKET_SIZE_OUTPUT, FLAGS_SEND); + break; + } + + // only allow the following ASCII characters in the user name (utf8 should be fine for all codes that match the ASCII table). + for (i = 0; i < message_length && processed < PACKET_SIZE_INPUT; i++) { + // if a NULL char is reached, then the packet is finished. + if (buffer[i] == 0) { + // force the loop to terminate without processing any more packet data. + processed = PACKET_SIZE_INPUT; + break; + } + + if ((buffer[i] < 'a' || buffer[i] > 'z') && (buffer[i] < 'A' || buffer[i] > 'Z') && (buffer[i] < '0' || buffer[i] > '9')) { + if (buffer[i] != '-' && buffer[i] != '_') { + error_receive = ERROR_NAME; + break; + } + } + + if (processed + 1 > PACKET_SIZE_INPUT) { + sent = send(shared->socket_id_client, ERROR_PACKET, PACKET_SIZE_OUTPUT, FLAGS_SEND); + break; + } + + user_name[processed] = buffer[i]; + processed++; + } // for + + if (error_receive != ERROR_NONE) { + sent = send(shared->socket_id_client, error_receive, PACKET_SIZE_OUTPUT, FLAGS_SEND); + break; + } + + // require processed to be populated in some manner at this point. + if (processed == 0) { + error_receive = ERROR_NAME; + break; + } + } while (processed < PACKET_SIZE_INPUT); + + if (error_receive == ERROR_NONE) { + int ldap_name_exists = 0; + ldap_name_exists = does_name_exist_in_ldap(user_name); + + if (ldap_name_exists < 0) { + sent = send(shared->socket_id_client, ERROR_LDAP, PACKET_SIZE_OUTPUT, FLAGS_SEND); + shutdown(shared->socket_id_client, SHUT_RDWR); + shared->socket_id_client = 0; + continue; + } + else if (ldap_name_exists == 0) { + sent = send(shared->socket_id_client, ERROR_NAME, PACKET_SIZE_OUTPUT, FLAGS_SEND); + shutdown(shared->socket_id_client, SHUT_RDWR); + shared->socket_id_client = 0; + continue; + } + + { + int status = 0; + status = grant_role_in_database(user_name, shared->parameter_group, shared->parameter_database, shared->parameter_connect_name, shared->parameter_connect_password); + + if (status < 0) { + sent = send(shared->socket_id_client, ERROR_SQL, PACKET_SIZE_OUTPUT, FLAGS_SEND); + shutdown(shared->socket_id_client, SHUT_RDWR); + shared->socket_id_client = 0; + continue; + } + } + } + + if (shared->socket_id_client > 0) { + // respond to the client for success or failure and then close the connection + if (processed > 0) { + sent = send(shared->socket_id_client, error_receive, PACKET_SIZE_OUTPUT, FLAGS_SEND); + } + + shutdown(shared->socket_id_client, SHUT_RDWR); + shared->socket_id_client = 0; + } + } // while + + // send SIGQUIT signal to parent process. + if (shared->pid_parent > 0) { + kill(shared->pid_parent, SIGQUIT); + } + + return 0; +} + +/** + * Handle command line arguments + * + * @param int argc + * Total of command line arguments. + * @param char *argv[] + * Array of command line argument strings. + * @param char *parameter_system + * Name of the system, this value will be updated. + * @param char *parameter_group + * Name of the group, this value will be updated. + * @param char *parameter_database + * Name of the database, this value will be updated. + * @param char *parameter_connect_name + * Name of the user to connect to the database as. + * @param char *parameter_connect_password + * Password of the user to connect to the database as. + * @param int *parameter_port + * Number of the port, this value will be updated. + * This only appears when USE_NETWORK is defined. + * + * @return int + * 1 is returned on success, 0 on success but exit, and -1 on error. + */ +#ifdef USE_NETWORK + int populate_parameters(int argc, char *argv[], char *parameter_system, char *parameter_group, char *parameter_database, char *parameter_connect_name, char *parameter_connect_password, int *parameter_port) { +#elif defined USE_SOCKET + int populate_parameters(int argc, char *argv[], char *parameter_system, char *parameter_group, char *parameter_database, char *parameter_connect_name, char *parameter_connect_password) { +#endif // USE_SOCKET + { + int do_help = 0; + char *program_name = "(program_name)"; + + if (argc > 0) { + program_name = argv[0]; + } + + if (argc >= 2) { + int i = 0; + + for (; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + do_help = 1; + break; + } + } + } + + #ifdef USE_NETWORK + if (do_help == 0 && !(argc == 4 || argc == 5)) { + printf("ERROR: This program requires four or five arguments (with max lengths of %u) 'system name', 'group name', 'database name', 'listen port', example: %s fcs fcs_users fcs_database 125.\n", PARAMETER_LENGTH_MAX, program_name); + do_help = 2; + } + #elif defined USE_SOCKET + if (do_help == 0 && !(argc == 3 || argc == 4)) { + printf("ERROR: This program requires three or four arguments (with max lengths of %u) 'system name', 'group name', 'database name', example: %s fcs fcs_users fcs_database.\n", PARAMETER_LENGTH_MAX, program_name); + do_help = 2; + } + #endif // USE_SOCKET + + if (do_help > 0) { + printf("\n"); + + #ifdef USE_NETWORK + printf("%s [ system name ] [ group name ] [ database name ] [ listen port ]\n", program_name); + #elif defined USE_SOCKET + printf("%s [ system name ] [ group name ] [ database name ]\n", program_name); + #endif // USE_SOCKET + + printf(" [ system name ] This argument is used as the name of the socket file, which will end in '.socket'.\n"); + printf(" [ group name ] This argument is used as the postgresql role to grant access for in the specified database.\n"); + printf(" [ database name ] This argument is used as the postgresql database.\n"); + + #ifdef USE_NETWORK + printf(" [ listen port ] This argument is the port to listen on to accept user names.\n"); + #endif // USE_NETWORK + + printf("\n"); + printf("Environment Variables:\n"); + printf(" The following environment variables must be defined:\n"); + printf(" %s This parameter is used as the user to connect to the database as to perform operations.\n", ENVIRONMENT_CONNECT_USER); + printf(" %s This parameter is used as the password for the user connecting to the database.\n", ENVIRONMENT_CONNECT_PASSWORD); + + printf("\n"); + printf("Notes:\n"); + printf(" All names are limited to a max length of %u.\n", PARAMETER_LENGTH_MAX); + printf(" Only Alphanumeric values and '_' or '-' are allowed in the names.\n"); + printf(" Specifically '_' and '-' are not allowed in the beginning or ending in a name.\n"); + printf(" The username to connect to as for the current user is used. Make sure the current user has access to the database in question.\n"); + printf("\n"); + + if (do_help == 2) { + return -1; + } + + return 0; + } + } + + // make sure the system name and role names are valid names, restricted to alphanumeric and - or _, but does not begin with - or _. + { + int i = 0; + int j = 0; + + + // process system name. + for (; i < PARAMETER_LENGTH_MAX; i++) { + if ((argv[1][i] >= 'a' || argv[1][i] <= 'z') || (argv[1][i] >= 'A' || argv[1][i] <= 'Z') || (argv[1][i] >= '0' || argv[1][i] <= '9')) { + parameter_system[j] = argv[1][i]; + j++; + i++; + break; + } + else if (argv[1][i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied system name '%s'.\n", argv[1][i], argv[1]); + return -1; + } + } + + for (; i < PARAMETER_LENGTH_MAX; i++) { + if ((argv[1][i] >= 'a' || argv[1][i] <= 'z') || (argv[1][i] >= 'A' || argv[1][i] <= 'Z') || (argv[1][i] >= '0' || argv[1][i] <= '9')) { + parameter_system[j] = argv[1][i]; + j++; + } + else if (argv[1][i] == '-' || argv[1][i] == '_') { + parameter_system[j] = argv[1][i]; + j++; + } + else if (argv[1][i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied system name '%s'.\n", argv[1][i], argv[1]); + return -1; + } + } + + if (argv[1][j - 1] == '-' || argv[1][j - 1] == '_') { + printf("ERROR: an invalid character '%c' has been specified in the supplied system name '%s'.\n", argv[1][i], argv[1]); + return -1; + } + + if (j == 0) { + printf("ERROR: system name must not be an empty string.\n", argv[3][i], argv[3]); + return -1; + } + + + // process group name. + for (i = 0, j = 0; i < PARAMETER_LENGTH_MAX; i++) { + if ((argv[2][i] >= 'a' || argv[2][i] <= 'z') || (argv[2][i] >= 'A' || argv[2][i] <= 'Z') || (argv[2][i] >= '0' || argv[2][i] <= '9')) { + parameter_group[j] = argv[2][i]; + j++; + i++; + break; + } + else if (argv[2][i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied group name '%s'.\n", argv[2][i], argv[2]); + return -1; + } + } + + for (; i < PARAMETER_LENGTH_MAX; i++) { + if ((argv[2][i] >= 'a' || argv[2][i] <= 'z') || (argv[2][i] >= 'A' || argv[2][i] <= 'Z') || (argv[2][i] >= '0' || argv[2][i] <= '9')) { + parameter_group[j] = argv[2][i]; + j++; + } + else if (argv[2][i] == '-' || argv[2][i] == '_') { + parameter_group[j] = argv[2][i]; + j++; + } + else if (argv[2][i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied group name '%s'.\n", argv[2][i], argv[2]); + return -1; + } + } + + if (argv[2][j - 1] == '-' || argv[2][j - 1] == '_') { + printf("ERROR: an invalid character '%c' has been specified in the supplied group name '%s'.\n", argv[2][i], argv[2]); + return -1; + } + + if (j == 0) { + printf("ERROR: group name must not be an empty string.\n", argv[3][i], argv[3]); + return -1; + } + + + // process database name. + for (i = 0, j = 0; i < PARAMETER_LENGTH_MAX; i++) { + if ((argv[3][i] >= 'a' || argv[3][i] <= 'z') || (argv[3][i] >= 'A' || argv[3][i] <= 'Z') || (argv[3][i] >= '0' || argv[3][i] <= '9')) { + parameter_database[j] = argv[3][i]; + j++; + i++; + break; + } + else if (argv[3][i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied database name '%s'.\n", argv[3][i], argv[3]); + return -1; + } + } + + for (; i < PARAMETER_LENGTH_MAX; i++) { + if ((argv[3][i] >= 'a' || argv[3][i] <= 'z') || (argv[3][i] >= 'A' || argv[3][i] <= 'Z') || (argv[3][i] >= '0' || argv[3][i] <= '9')) { + parameter_database[j] = argv[3][i]; + j++; + } + else if (argv[3][i] == '-' || argv[3][i] == '_') { + parameter_database[j] = argv[3][i]; + j++; + } + else if (argv[3][i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied database name '%s'.\n", argv[3][i], argv[3]); + return -1; + } + } + + if (argv[3][j - 1] == '-' || argv[3][j - 1] == '_') { + printf("ERROR: an invalid character '%c' has been specified in the supplied database name '%s'.\n", argv[3][i], argv[3]); + return -1; + } + + if (j == 0) { + printf("ERROR: database name must not be an empty string.\n", argv[3][i], argv[3]); + return -1; + } + + + #ifdef USE_NETWORK + // first sanitize the parameter and ensure that only numbers are allowed. + for (; i < PARAMETER_LENGTH_MAX; i++) { + if (argv[4][i] >= '0' || argv[4][i] <= '9') { + continue; + } + else if (argv[4][i] == 0) { + break; + } + + printf("ERROR: an invalid character '%c' has been specified in the supplied local port '%s' (only numbers are allowed).\n", argv[4][i], argv[4]); + return -1; + } + + *parameter_port = atoi(argv[4]); + #endif // USE_NETWORK + + // process database connection user name. + { + char *user_name = getenv(ENVIRONMENT_CONNECT_USER); + + if (user_name) { + size_t user_name_length = strnlen(user_name, ENVIRONMENT_MAX_CONNECT_USER); + + for (i = 0, j = 0; i < user_name_length; i++) { + if ((user_name[i] >= 'a' || user_name[i] <= 'z') || (user_name[i] >= 'A' || user_name[i] <= 'Z') || (user_name[i] >= '0' || user_name[i] <= '9')) { + parameter_connect_name[j] = user_name[i]; + j++; + i++; + break; + } + else if (user_name[i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied database name '%s'.\n", user_name[i], user_name); + return -1; + } + } + + for (; i < user_name_length; i++) { + if ((user_name[i] >= 'a' || user_name[i] <= 'z') || (user_name[i] >= 'A' || user_name[i] <= 'Z') || (user_name[i] >= '0' || user_name[i] <= '9')) { + parameter_connect_name[j] = user_name[i]; + j++; + } + else if (user_name[i] == '_') { + parameter_connect_name[j] = user_name[i]; + j++; + } + else if (user_name[i] == 0) { + break; + } + else { + printf("ERROR: an invalid character '%c' has been specified in the supplied database name '%s'.\n", user_name[i], user_name); + return -1; + } + } + } + else { + printf("ERROR: Failed to load required environment variable '%s'.\n", ENVIRONMENT_CONNECT_USER); + return -1; + } + } + + + // process database connection password. + { + char *password = getenv(ENVIRONMENT_CONNECT_PASSWORD); + + if (password) { + size_t password_length = strnlen(password, ENVIRONMENT_MAX_CONNECT_PASSWORD); + + if (password_length > 0) { + strncpy(parameter_connect_password, password, password_length); + } + } + else { + printf("ERROR: Failed to load required environment variable '%s'.\n", ENVIRONMENT_CONNECT_PASSWORD); + return -1; + } + } + } + + return 1; +} + +/** + * Main Function + * + * @param int argc + * Total of command line arguments. + * @param char *argv[] + * Array of command line argument strings. + * + * @return int + * The return status of the program. + */ +int main(int argc, char *argv[]) { + shared_data shared; + char *stack = NULL; + + memset(&shared, 0, sizeof(shared_data)); + + // this pid will change once daemonized, but until then record the current pid. + shared.pid_parent = getpid(); + + { + int populated = 0; + + #ifdef USE_NETWORK + populated = populate_parameters(argc, argv, shared.parameter_system, shared.parameter_group, shared.parameter_database, shared.parameter_connect_name, shared.parameter_connect_password, &shared.parameter_port); + #elif defined USE_SOCKET + populated = populate_parameters(argc, argv, shared.parameter_system, shared.parameter_group, shared.parameter_database, shared.parameter_connect_name, shared.parameter_connect_password); + #endif // USE_SOCKET + + + if (populated == 0) { + MACRO_EXIT_STANDARD_1(shared, stack, 0); + } + else if (populated < 0) { + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + } + + + // load the pid_path. + shared.pid_path = malloc(sizeof(char) * PATH_MAX); + if (shared.pid_path == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate memory for the pid path.\n"); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + + + // check to see if an existing pid file exists before doing anything else. + { + struct stat pid_stat; + int result_stat = 0; + + memset(&pid_stat, 0, sizeof(struct stat)); + result_stat = stat(shared.pid_path, &pid_stat); + if (result_stat > -1 || errno != ENOENT) { + if (result_stat > -1) { + printf("ERROR: the pid file already exists at '%s', this pid: %u.'\n", shared.pid_path, shared.pid_parent); + } + else { + printf("ERROR: while calling stat() on '%s', this pid: %u.'\n", shared.pid_path, shared.pid_parent); + } + + // do not attempt to unlink the path, so manually free and reset before MACRO_EXIT_STANDARD_1() is called. + memset(shared.pid_path, 0, sizeof(char) * PATH_MAX); + free(shared.pid_path); + shared.pid_path = NULL; + + memset(&pid_stat, 0, sizeof(struct stat)); + result_stat = 0; + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + } + + + #ifdef USE_NETWORK + shared.socket_id_target = socket(SOCKET_FAMILY, SOCKET_TYPE, SOCKET_PROTOCOL); + + if (shared.socket_id_target < 0) { + printf("ERROR: failed to initiailize the port '%u' using protocol '%u': error %i (%u).'\n", shared.parameter_port, SOCKET_PROTOCOL, shared.socket_id_target, errno); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + #elif defined USE_SOCKET + { + int socket_path_length = SOCKET_PATH_LENGTH + 1; + socket_path_length += strnlen(shared.parameter_system, PARAMETER_LENGTH_MAX); + socket_path_length += strnlen(shared.parameter_group, PARAMETER_LENGTH_MAX); + socket_path_length *= sizeof(char); + + shared.socket_path = malloc(socket_path_length); + if (shared.socket_path == NULL) { + printf("ERROR: failed to allocate enough memory for the socket path string.\n"); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + + memset(shared.socket_path, 0, socket_path_length); + snprintf(shared.socket_path, socket_path_length, SOCKET_PATH, shared.parameter_system, shared.parameter_group); + } + + // make sure that no file exists at shared.socket_path before attempt to create a socket. + { + struct stat file_stat; + + if (stat(shared.socket_path, &file_stat) >= 0) { + printf("ERROR: failed to initiailize the socket '%s' because a file already exists at that path, exiting.\n", shared.socket_path); + + if (shared.socket_path != NULL) { + free(shared.socket_path); + shared.socket_path = NULL; + } + + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + } + + shared.socket_id_target = socket(SOCKET_FAMILY, SOCKET_TYPE, SOCKET_PROTOCOL); + + if (shared.socket_id_target < 0) { + printf("ERROR: failed to initiailize the socket '%s' using protocol '%u', 'socket id = '%i, exiting.'\n", shared.socket_path, SOCKET_PROTOCOL, shared.socket_id_target); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + #endif // USE_SOCKET + + + // now run the process in the background before cloning and before blocking for signals. + { + #ifdef DEBUG_ENABLED + int daemonized = daemon(0, 1); + #else + int daemonized = daemon(0, 0); + #endif // DEBUG_ENABLED + + if (daemonized < 0) { + printf("ERROR: failed to daemonize, error: %i.\n", errno); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + } + + + // The stack must be malloced after the process has daemonized because daemon() calls fork(). + stack = malloc(STACK_SIZE); + if (stack == NULL) { + log_write(LOG_ERR, "ERROR: failed to allocate the stack for cloning, error: %i.\n", errno); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + memset(stack, 0, sizeof(char) * STACK_SIZE); + + + // create the pid file or fail if one already exists. + shared.pid_parent = getpid(); + if (snprintf(shared.pid_path, sizeof(char) * PATH_MAX, PATH_PID, shared.parameter_system) < 0) { + log_write(LOG_ERR, "ERROR: failed to setup the pid string '%s' using system name '%s', this pid: %u.'\n", PATH_PID, shared.parameter_system, shared.pid_parent); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + + { + FILE *pid_file = NULL; + + pid_file = fopen(shared.pid_path, "w"); + if (pid_file <= 0) { + log_write(LOG_ERR, "ERROR: failed to create pid file '%s', this pid: %u.'\n", shared.pid_path, shared.pid_parent); + pid_file = NULL; + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + + if (fprintf(pid_file, "%u\n", shared.pid_parent) < 0) { + log_write(LOG_ERR, "ERROR: failed to create pid file '%s', this pid: %u.'\n", shared.pid_path, shared.pid_parent); + fclose(pid_file); + pid_file = NULL; + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + + fclose(pid_file); + pid_file = NULL; + } + + + pid_t pid_child = clone(handler_child, stack + STACK_SIZE, FLAGS_CLONE, &shared); + + if (pid_child < 0) { + log_write(LOG_ERR, "ERROR: failed to clone the process, error: %i.\n", errno); + MACRO_EXIT_STANDARD_1(shared, stack, -1); + } + + // signal blocking is used to help the program safely quit on interrupt. + sigset_t signal_mask; + siginfo_t signal_information_parent; + //siginfo_t signal_information_child; + int signal_result = 0; + short signal_problem_count = 0; + int process_child = 0, process_wait = 0; + + memset(&signal_mask, 0, sizeof(sigset_t)); + memset(&signal_information_parent, 0, sizeof(siginfo_t)); + //memset(&signal_information_child, 0, sizeof(siginfo_t)); + + // block signals. + sigemptyset(&signal_mask); + sigaddset(&signal_mask, SIGHUP); + sigaddset(&signal_mask, SIGINT); + sigaddset(&signal_mask, SIGQUIT); + sigaddset(&signal_mask, SIGTERM); + sigaddset(&signal_mask, SIGSEGV); + sigaddset(&signal_mask, SIGBUS); + sigaddset(&signal_mask, SIGILL); + sigaddset(&signal_mask, SIGFPE); + sigaddset(&signal_mask, SIGABRT); + sigaddset(&signal_mask, SIGPWR); + sigaddset(&signal_mask, SIGXCPU); + sigaddset(&signal_mask, SIGCHLD); + + sigprocmask(SIG_BLOCK, &signal_mask, NULL); + + // sit and wait for signals. + while(1) { + signal_result = sigwaitinfo(&signal_mask, &signal_information_parent); + + if (signal_result < 0) { + if (errno == EAGAIN) { + // do nothing. + continue; + } + else if (errno != EINTR) { + log_write(LOG_ERR, "ERROR: sigwaitinfo() failed: %i.\n", errno); + + signal_problem_count++; + if (signal_problem_count > PROBLEM_COUNT_MAX_SIGNAL_SIZE) { + log_write(LOG_ERR, "ERROR: max signal problem count has been reached, exiting.\n"); + MACRO_EXIT_STANDARD_2(pid_child, shared, stack, -1); + } + + continue; + } + } + + signal_problem_count = 0; + + if (signal_information_parent.si_signo == SIGHUP) { + // do nothing. + } + else if (signal_information_parent.si_signo == SIGINT || signal_information_parent.si_signo == SIGQUIT || signal_information_parent.si_signo == SIGTERM) { + MACRO_EXIT_STANDARD_2(pid_child, shared, stack, 0); + } + else if (signal_information_parent.si_signo == SIGSEGV || signal_information_parent.si_signo == SIGBUS || signal_information_parent.si_signo == SIGILL || signal_information_parent.si_signo == SIGFPE) { + MACRO_EXIT_STANDARD_2(pid_child, shared, stack, 0); + } + else if (signal_information_parent.si_signo == SIGABRT || signal_information_parent.si_signo == SIGIOT || signal_information_parent.si_signo == SIGPWR || signal_information_parent.si_signo == SIGXCPU) { + MACRO_EXIT_STANDARD_2(pid_child, shared, stack, 0); + } + else if (signal_information_parent.si_signo == SIGCHLD) { + // do nothing + } + + memset(&signal_information_parent, 0, sizeof(siginfo_t)); + continue; + } + + // failsafe, but should not get here. + MACRO_EXIT_STANDARD_2(pid_child, shared, stack, 0); +} diff --git a/program/autocreate_ldap_accounts_in_postgresql/source/php/autocreate_ldap_accounts_in_postgresql-client.php b/program/autocreate_ldap_accounts_in_postgresql/source/php/autocreate_ldap_accounts_in_postgresql-client.php new file mode 100644 index 0000000..fedbe58 --- /dev/null +++ b/program/autocreate_ldap_accounts_in_postgresql/source/php/autocreate_ldap_accounts_in_postgresql-client.php @@ -0,0 +1,101 @@ + 0) { + // the packet expects a packet to be NULL terminated or at most $packet_size_target. + $test_packet = pack('a' . $test_name_length . 'x' . $test_name_difference, $test_name); + } + else { + $test_packet = pack('a' . $test_name_length, $test_name); + } + + print("Packet looks like: '$test_packet'\n"); + $written = socket_write($socket, $test_packet, $packet_size_target); + + if ($written === FALSE) { + print("Something went wrong with socket_write().\n"); + socket_close($socket); + return; + } + elseif ($written == 0) { + print("Nothing was written to the socket using socket_write().\n"); + socket_close($socket); + return; + } + + + // read the return result from the target socket. + $response = socket_read($socket, $packet_size_client); + + if (!is_string($response) || strlen($response) == 0) { + print("Something went wrong with socket_read() and did not get a valid return from the socket.\n"); + socket_close($socket); + return; + } + + // an integer is expected to be returned by the socket. + $response_packet = unpack('C', $response); + $response_value = (int) $response_packet[1]; + + print("Target Socket Replied with = " . print_r($response_value, TRUE) . "\n"); + + // response codes as defined in the c source file: + // 0 = no problems detected. + // 1 = invalid user name, bad characters, or name too long. + // 2 = failed to connect to the ldap server and could not query the ldap name. + // 3 = user name not found in ldap database. + // 4 = failed to connect to the database. + // 5 = error returned while executing the SQL command. + // 6 = error occured while reading input from the user (such as via recv()). + // 7 = error occured while writing input from the user (such as via send()). + // 8 = the received packet is invalid, such as wrong length. + // 10 = connection timed out when reading or writing. + // 11 = the connection is being forced closed. + // 12 = the connection is closing because the service is quitting. + + + socket_close($socket); diff --git a/program/sessionize_accounts/readme.txt b/program/sessionize_accounts/readme.txt new file mode 100644 index 0000000..2cb885b --- /dev/null +++ b/program/sessionize_accounts/readme.txt @@ -0,0 +1,24 @@ +Installation +============ +This assumes that the /program/ paths are being used. + +Compile the source code: + gcc -g source/c/sessionize_ldap_accounts_in_postgresql.c -o /programs/bin/sessionize_ldap_accounts_in_postgresql + +Add and enable the init script: + cp -v source/bash/sessionize_ldap_accounts_in_postgresql.sh /etc/init.d/sessionize_ldap_accounts_in_postgresql + chkconfig --add sessionize_ldap_accounts_in_postgresql + chkconfig sessionize_ldap_accounts_in_postgresql on + +Configure the settings (assuming system called "example"): + mkdir -vp /programs/settings/sessionize_ldap_accounts_in_postgresql/ + cp -v settings/{example,systems}.settings /programs/settings/sessionize_ldap_accounts_in_postgresql/ + +Note: rename 'example.settings' to the name of the system as defined in 'systems.settings'. + +Start the service + service sessionize_ldap_accounts_in_postgresql start + +For most users, the /programs/ path needs to be changed to a custom path for your system. +The source code and bash scripts will need to be updated with these hardcoded paths. + diff --git a/program/sessionize_accounts/settings/systems.settings b/program/sessionize_accounts/settings/systems.settings new file mode 100644 index 0000000..b97ca19 --- /dev/null +++ b/program/sessionize_accounts/settings/systems.settings @@ -0,0 +1,3 @@ +# fss-0000 + +sa_systems example diff --git a/program/sessionize_accounts/source/bash/sessionize_accounts.sh b/program/sessionize_accounts/source/bash/sessionize_accounts.sh new file mode 100644 index 0000000..e230244 --- /dev/null +++ b/program/sessionize_accounts/source/bash/sessionize_accounts.sh @@ -0,0 +1,421 @@ +#!/bin/bash +# +# sessionize_accounts Helper service for handling account sessions, namely password storage/retrieval. +# +# chkconfig: 345 40 60 +# description: Provide a per-user, per-ip_address, per-session storage of passwords for users. + +### BEGIN INIT INFO +# Provides: sessionize_accounts +# Required-Start: $local_fs $network +# Required-Stop: $local_fs $network +# Default-Start: 3 4 5 +# Default-Stop: 0 1 2 6 +# Short-Description: Provides session storage of usernames and passwords on a per ip-address basis. +# Description: Provides session storage of usernames and passwords on a per ip-address basis. +### END INIT INFO + +# Source function library. +. /etc/rc.d/init.d/functions + +main() { + local process_owner= + local process_group="apache" + local path_programs="/programs/" + local path_service="/usr/local/bin/php ${path_programs}bin/sessionize_accounts-server.php" + local path_settings="${path_programs}settings/sessionize_accounts/" + local path_systems="${path_settings}systems.settings" + local path_pids="/var/run/sessionize_accounts/" + local path_socket_directory="/var/www/sockets/sessionize_accounts/" + local parameter_system=$2 + local sa_systems= + local i= + local j= + + if [[ ! -f $path_systems ]] ; then + echo "No valid path_systems file defined at: $path_systems" + exit -1 + fi + + if [[ ! -d $path_pids ]] ; then + mkdir -p $path_pids + fi + + if [[ $process_owner != "" ]] ; then + chown $process_owner $path_pids + fi + + sa_systems=$(grep -o '^sa_systems[[:space:]][[:space:]]*.*$' $path_systems | sed -e 's|^sa_systems[[:space:]][[:space:]]*||') + + if [[ $sa_systems == "" ]] ; then + echo "No valid systems defined by setting 'sa_systems' in file: $path_systems" + exit -1 + fi + + if [[ $parameter_system != "" ]] ; then + j=$sa_systems + sa_systems= + + for i in $j ; do + if [[ $i == $parameter_system ]] ; then + sa_systems=$i + break; + fi + done + + i= + j= + + if [[ $sa_systems == "" ]] ; then + echo "System '$parameter_system' is not a valid system defined by setting 'sa_systems' in file: $path_systems" + exit -1 + fi + fi + + # system-specific settings are not needed by this script. + #j=$sa_systems + #sa_systems= + #for i in $j ; do + # if [[ -f $path_settings${i}.settings ]] ; then + # sa_systems="$sa_systems$i " + # else + # echo "Skipping system '$i' because it does not have a settings file defined here: '$path_settings${i}.settings'" + # fi + #done + + i= + j= + + case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + status) + status + ;; + *) + echo "Usage: sessionize_accounts {start|stop|restart|status}" + return 2 + esac + + return $? +} + +start() { + local sa_system= + local result= + local any_success=0 + local any_failure=0 + + for sa_system in $sa_systems ; do + load_system_settings + check_pid + + if [[ $result -eq -1 ]] ; then + continue + elif [[ $result -gt 0 ]] ; then + echo "Not starting process for $sa_system, it is already running with pid=$pid." + continue + fi + + start_command + + if [[ $result -eq 0 ]] ; then + wait_pid + + if [[ $result -eq 0 ]] ; then + get_pid + fi + + if [[ $pid == "" ]] ; then + echo "Started process for $sa_system but was unable to determine pid, command: $path_service $sa_system." + else + echo "Successfully started process for $sa_system, pid=$pid, command: $path_service $sa_system." + fi + fi + done + + if [[ $any_success -ne 0 || $any_failure -eq 1 ]] ; then + exit -1 + fi + + return 0 +} + +stop() { + local sa_system= + local result= + local any_success=0 + local any_failure=0 + local original_pid= + + for sa_system in $sa_systems ; do + load_system_settings + get_pid + + if [[ $pid == "" ]] ; then + continue + fi + + stop_command + + if [[ $result -eq 0 ]] ; then + echo "Successfully stopped process for $sa_system, pid=$pid." + fi + done + + if [[ $any_success -ne 0 || $any_failure -eq 1 ]] ; then + exit -1 + fi + + return 0 +} + +restart() { + local sa_system= + local result= + local any_success=0 + local any_failure=0 + local original_pid= + + for sa_system in $sa_systems ; do + load_system_settings + check_pid + + if [[ $result -lt 0 ]] ; then + continue + elif [[ $result -gt 0 ]] ; then + stop_command + + if [[ $result -ne 0 ]] ; then + continue + fi + + original_pid=$pid + check_pid + + if [[ $result -eq -2 ]] ; then + echo "Successfully stopped process for $sa_system, pid=$original_pid." + else + echo "Sent stop command for $sa_system, pid=$original_pid, but pid file ($pid_file) still exists (cannot start process, skipping)." + continue + fi + fi + + start_command + + if [[ $result -eq 0 ]] ; then + wait_pid + + if [[ $result -eq 0 ]] ; then + get_pid + fi + + if [[ $pid == "" ]] ; then + echo "Started process for $sa_system but was unable to determine pid, command: $path_service $sa_system." + else + echo "Successfully started process for $sa_system, pid=$pid, command: $path_service $sa_system." + fi + fi + done + + if [[ $any_success -ne 0 ]] ; then + exit -1 + fi + + return 0 +} + +status() { + local sa_system= + local pid_file= + local pid= + local result= + + for sa_system in $sa_systems ; do + load_system_settings + get_pid + + if [[ $pid == "" ]] ; then + continue + fi + + echo "The system '$sa_system' appears to be running as process $pid." + done + + return 0 +} + +load_system_settings() { + local path_system=$path_settings${sa_system}.settings + + # nothing to load +} + +start_command() { + # guarantee that all directories in the socket file's path exist. + if [[ ! -d $path_socket_directory/$sa_system/ ]] ; then + mkdir -p $path_socket_directory/$sa_system/ + chown $process_owner $path_socket_directory/$sa_system/ + fi + + # guarantee that the '$process_group' has read and execute only access to the directory, deny world access. + chgrp $process_group $path_socket_directory/$sa_system/ + chmod u+rwx,g+rx,o-rwx $path_socket_directory/$sa_system/ + + # make sure no session socket already exists before starting. + # this assumes that the pid file has already been checked and therefore no existing process is using the socket file (aka: assume this is a stale socket file). + if [[ -e $path_socket_directory/$sa_system/sessions.socket ]] ; then + rm -f $path_socket_directory/$sa_system/sessions.socket + fi + + if [[ $process_owner == "" ]] ; then + $path_service "$sa_system" + result=$? + else + su $process_owner -l -c "$path_service \"$sa_system\"" + result=$? + fi + + if [[ $result -ne 0 ]] ; then + echo "Failed to start process, command: $path_service \"$sa_system\"." + any_failure=1 + else + any_success=1 + fi +} + +stop_command() { + # -3 = SIGQUIT, -15 = SIGTERM, -9 = SIGKILL + kill -3 $pid + result=$? + + if [[ $result -ne 0 ]] ; then + echo "Signal to quit failed, command: kill -3 $pid." + any_failure=1 + else + any_success=1 + + # pause and give the process time to close down. + sleep 0.1 + + # cleanup the session socket ad pid file. + rm -f $path_socket_directory/$sa_system/sessions.socket + rm -f $pid_file + fi +} + +wait_pid() { + local k= + local max=32 + + pid= + pid_file=$path_pids$sa_system.pid + result=-1 + + # the started process will go into the background, so wait until the pid file is created, but only wait for so long. + let k=0 + while [[ $k -lt $max ]] ; do + if [[ -f $pid_file ]] ; then + result=0 + break + fi + + sleep 0.05 + + let k=$k+1 + done + + return 0 +} + +get_pid() { + pid= + pid_file=$path_pids$sa_system.pid + + if [[ ! -f $pid_file ]] ; then + echo "No pid file ($pid_file) found for system '$sa_system', it must not be running." + return 0 + fi + + pid=$(cat $pid_file) + result=$? + + if [[ $result -ne 0 ]] ; then + echo "Failed to read the pid file ($pid_file) for system '$sa_system', command: cat $pid_file." + pid= + return 0 + fi + + if [[ $pid == "" ]] ; then + echo "The pid file ($pid_file) for system '$sa_system' is empty." + pid= + return 0 + fi + + result=$(ps --no-headers -o pid -p $pid) + if [[ $? -lt 0 ]] ; then + echo "An error occured while searching for the process for system '$sa_system', command: ps --no-headers -o pid -p $pid." + pid= + return 0 + fi + + if [[ $result == "" ]] ; then + echo "No process $pid was found for the system '$sa_system', the pid file might be stale or inaccurate." + pid= + return 0 + fi +} + +check_pid() { + pid= + pid_file=$path_pids$sa_system.pid + + if [[ ! -f $pid_file ]] ; then + result=-2 + return 0 + fi + + pid=$(cat $pid_file) + result=$? + + if [[ $result -ne 0 ]] ; then + echo "Failed to read the pid file ($pid_file) for system '$sa_system', command: cat $pid_file." + result=-1 + return 0 + fi + + if [[ $pid == "" ]] ; then + result=0 + return 0 + fi + + result=$(ps --no-headers -o pid -p $pid) + if [[ $? -lt 0 ]] ; then + echo "An error occured while searching for the process for system '$sa_system', command: ps --no-headers -o pid -p $pid." + result=-1 + return 0 + fi + + if [[ $result == "" ]] ; then + result= + + # the pid file is invalid, so remove the pid file. + rm -f $pid_file + + # return 0 to allow for starting a new process. + result=0 + return 0 + fi + + result=1 + return 0 +} + +main "$1" "$2" diff --git a/program/sessionize_accounts/source/php/sessionize_accounts-client.php b/program/sessionize_accounts/source/php/sessionize_accounts-client.php new file mode 100644 index 0000000..0b2a315 --- /dev/null +++ b/program/sessionize_accounts/source/php/sessionize_accounts-client.php @@ -0,0 +1,138 @@ + 'some_user', + 'ip' => '127.0.0.1', + 'password' => 'This is weak\' password.', + ); + + $request_json = json_encode($request_array); + $written = socket_write($socket, $request_json); + + if ($written === FALSE) { + print("Session Request: Something went wrong with socket_write().\n"); + socket_close($socket); + return; + } + elseif ($written == 0) { + print("Session Request: Nothing was written to the socket using socket_write().\n"); + socket_close($socket); + return; + } + + // read the return result from the target socket. + $response = socket_read($socket, PACKET_MAX_LENGTH); + + if (!is_string($response) || strlen($response) == 0) { + print("Session Request: Something went wrong with socket_read() and did not get a valid return from the socket.\n"); + socket_close($socket); + return; + } + + // an array is expected to be returned by the socket. + $response_array = json_decode($response, TRUE); + if (!is_array($response_array['result']) || empty($response_array['result']['session_id']) || !is_string($response_array['result']['session_id'])) { + print("Session Request: no valid session id was returned.\n"); + socket_close($socket); + return; + } + + socket_close($socket); + + $session_id = $response_array['result']['session_id']; + $expire = $response_array['result']['expire']; + $max = $response_array['result']['max']; + + + + // build password request packet using the returned session id. + $socket = socket_create($socket_family, $socket_type, $socket_protocol); + + if ($socket === FALSE) { + print("Password Request: Something went wrong with socket_create().\n"); + socket_close($socket); + return; + } + + $connected = socket_connect($socket, $socket_path, $socket_port); + + if ($connected === FALSE) { + print("Password Request: Something went wrong with socket_connect().\n"); + socket_close($socket); + return; + } + + $request_array = array( + 'ip' => '127.0.0.1', + 'session_id' => $session_id, + ); + + $request_json = json_encode($request_array); + $written = socket_write($socket, $request_json); + + if ($written === FALSE) { + print("Password Request: Something went wrong with socket_write().\n"); + socket_close($socket); + return; + } + elseif ($written == 0) { + print("Password Request: Nothing was written to the socket using socket_write().\n"); + socket_close($socket); + return; + } + + // read the return result from the target socket. + $response = socket_read($socket, PACKET_MAX_LENGTH); + + if (!is_string($response) || strlen($response) == 0) { + print("Password Request: Something went wrong with socket_read() and did not get a valid return from the socket.\n"); + socket_close($socket); + return; + } + + // an integer is expected to be returned by the socket. + $response_array = json_decode($response, TRUE); + + $password = $response_array['result']; + if (is_bool($password)) { + print("Password Request: no password was returned.\n"); + socket_close($socket); + return; + } + + + socket_close($socket); diff --git a/program/sessionize_accounts/source/php/sessionize_accounts-server.php b/program/sessionize_accounts/source/php/sessionize_accounts-server.php new file mode 100644 index 0000000..983a237 --- /dev/null +++ b/program/sessionize_accounts/source/php/sessionize_accounts-server.php @@ -0,0 +1,804 @@ + SOCKET_TIMEOUT_SECONDS, 'usec' => SOCKET_TIMEOUT_MICROSECONDS)); + + $response = array( + 'error' => FALSE, + 'result' => FALSE, + ); + + $encoded_packet = socket_read($client_socket, PACKET_MAX_LENGTH); + + if ($encoded_packet === FALSE) { + print("socket_read() failed: reason: " . socket_strerror(socket_last_error($client_socket)) . "\n"); + + socket_close($client_socket); + unset($client_socket); + unset($encoded_packet); + continue; + } + + if (!is_string($encoded_packet)) { + $response['error'] = array( + 'target' => 'encoded_packet', + 'message' => "No valid encoded packet was specified. It must be a valid json string.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + $decoded_packet = json_decode($encoded_packet, TRUE); + + if (!is_array($decoded_packet) || empty($decoded_packet)) { + $response['error'] = array( + 'target' => 'decoded_packet', + 'message' => "No valid decoded packet was specified. It must be a valid, non-empty, array.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + // support manually sending custom packets to periodically flush expired sessions (usefull for cron jobs). + if (array_key_exists('flush', $decoded_packet)) { + if (isset($decoded_packet['flush']) !== TRUE) { + $not_found_error = array( + 'target' => 'decoded_packet[flush]', + 'message' => "Invalid flush value provided.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + if (count($decoded_packet) > 1) { + $not_found_error = array( + 'target' => 'decoded_packet[*]', + 'message' => "Too many values provided, only the flush parameter is allowed.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + $response['result'] = process_expired_sessions($database, $timeouts, $cleartext); + if ($response['result'] === FALSE) { + $response['error'] = array( + 'target' => 'failure', + 'message' => "Failed to flush the expired sessions.", + ); + } + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + if (!isset($decoded_packet['ip']) || !is_string($decoded_packet['ip']) || strlen($decoded_packet['ip']) == 0 || ip2long($decoded_packet['ip']) === FALSE) { + $response['error'] = array( + 'target' => 'decoded_packet[ip]', + 'message' => "No valid ip address was specified. A valid, non-empty, ip address string must be provided.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + + // support closing sessions before they expire. + if (array_key_exists('close', $decoded_packet)) { + if (isset($decoded_packet['close']) !== TRUE) { + $not_found_error = array( + 'target' => 'decoded_packet[close]', + 'message' => "Invalid close value provided.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + if (isset($decoded_packet['session_id']) && strlen($decoded_packet['session_id']) > 0) { + $not_found_error = array( + 'target' => 'decoded_packet[session_id]', + 'message' => "No valid session was provided.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + if (count($decoded_packet) > 3) { + $not_found_error = array( + 'target' => 'decoded_packet[*]', + 'message' => "Too many values provided, only the session_id, ip, and close parameters are allowed.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + // provide an error, but specifically do not give details about which field is invalid for security reasons. + if (!isset($database['sessions'][$decoded_packet['ip']][$decoded_packet['session_id']])) { + $response['error'] = array( + 'target' => 'not_found', + 'message' => "No valid session was found associated with the specified user name and ip address.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + $response['result'] = expire_session($database, $timeouts, $decoded_packet['session_id'], $decoded_packet['ip']); + if ($response['result'] === FALSE) { + $response['error'] = array( + 'target' => 'failure', + 'message' => "Failed to close the session by the given session id and ip address.", + ); + } + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + + // expire sessions now so that expired session do not get included in the retrieval process. + process_expired_sessions($database, $timeouts, $cleartext); + + + // retrieve password. + if (isset($decoded_packet['session_id']) && strlen($decoded_packet['session_id']) > 0) { + if (!isset($database['sessions'][$decoded_packet['ip']][$decoded_packet['session_id']])) { + $response['error'] = array( + 'target' => 'not_found', + 'message' => "No valid session was found associated with the specified user name and ip address.", + ); + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + + // a password request shows that this connection is still active, so extend the timestamp. + $db_session = &$database['sessions'][$decoded_packet['ip']][$decoded_packet['session_id']]; + $unique_id = $db_session['timeouts']['id']; + + if (isset($db_session['timeouts']['expire']) && isset($db_session['timeouts']['max']) && $db_session['timeouts']['expire'] < $db_session['timeouts']['max']) { + $stamp_old = $db_session['timeouts']['expire']; + $stamp_new = strtotime('+' . $db_session['timeouts']['interval'] . ' seconds'); + if ($stamp_new > $db_session['timeouts']['max']) { + $stamp_new = $db_session['timeouts']['max']; + } + + if (isset($timeouts[$stamp_old]) && $stamp_old != $stamp_new) { + $db_session['timeouts']['expire'] = $stamp_new; + $new_key = !array_key_exists($stamp_new, $timeouts); + + $timeouts[$stamp_new]['expire'][$unique_id] = $timeouts[$stamp_old]['expire'][$unique_id]; + unset($timeouts[$stamp_old]['expire'][$unique_id]); + + if (empty($timeouts[$stamp_old]['expire'])) { + unset($timeouts[$stamp_old]['expire']); + } + + if (empty($timeouts[$stamp_old])) { + unset($timeouts[$stamp_old]); + } + + // only perform expensive sort if its a new key, which might be out of order. + if ($new_key) { + ksort($timeouts); + } + + unset($new_key); + } + + unset($stamp_new); + unset($stamp_old); + } + + $response['result'] = array( + 'name' => $db_session['name'], + 'id_user' => (int) $db_session['id_user'], + 'password' => $db_session['password'], + 'expire' => $db_session['timeouts']['expire'], + 'max' => $db_session['timeouts']['max'], + 'interval' => $db_session['timeouts']['interval'], + 'settings' => $db_session['settings'], + ); + + socket_write($client_socket, json_encode($response)); + unset($db_timeout); + unset($unique_id); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + // store password. + elseif (array_key_exists('password', $decoded_packet) && (is_null($decoded_packet['password']) || is_string($decoded_packet['password']))) { + if (!isset($decoded_packet['name']) || strlen($decoded_packet['name']) == 0 || preg_match('/^(\w|-)+$/i', $decoded_packet['name']) != 1) { + $response['error'] = array( + 'target' => 'decoded_packet[name]', + 'message' => "No valid user name was specified. A valid, non-empty, user name string must be provided.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + if ((is_int($decoded_packet['id_user']) && $decoded_packet['id_user'] < 0) || !is_int($decoded_packet['id_user']) && (!is_string($decoded_packet['id_user']) || !(is_numeric($decoded_packet['id_user']) && (int) $decoded_packet['id_user'] >= 0))) { + $response['error'] = array( + 'target' => 'decoded_packet[id_user]', + 'message' => "No valid id_user was specified. A valid id_user integer, greater than or equal to 0, must be provided.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + if (isset($decoded_packet['settings']) && !is_array($decoded_packet['settings'])) { + $response['error'] = array( + 'target' => 'decoded_packet[settings]', + 'message' => "If specified, settings must be a valid array.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + $session_id = build_session_id(); + if (isset($database['sessions'][$decoded_packet['ip']]) && is_array($database['sessions'][$decoded_packet['ip']]) && array_key_exists($session_id, $database['sessions'][$decoded_packet['ip']])) { + $response['error'] = array( + 'target' => 'conflict', + 'message' => "Failed to generate a unique session id due to a conflict. Please try again.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + unset($session_id); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + $unique_id = $decoded_packet['name'] . '-' . uniqid(); + $database['sessions'][$decoded_packet['ip']][$session_id] = array( + 'name' => $decoded_packet['name'], + 'id_user' => (int) $decoded_packet['id_user'], + 'password' => $decoded_packet['password'], + 'timeouts' => array( + 'id' => $unique_id, + 'expire' => NULL, + 'max' => NULL, + ), + 'settings' => isset($decoded_packet['settings']) ? $decoded_packet['settings'] : array(), + ); + + + // the timeout only needs to contain what is necessary to obtain the session data. + $timeout = array( + 'ip' => $decoded_packet['ip'], + 'session_id' => $session_id, + ); + + + // grab optional soft timeouts and enforce hard timeouts. + $timeout_expire = INTERVAL_TIMEOUT_HARD_EXPIRE; + $timeout_max = INTERVAL_TIMEOUT_HARD_MAX; + + if (isset($decoded_packet['max']) && is_int($decoded_packet['max']) && $decoded_packet['max'] > 0) { + if ($decoded_packet['max'] < INTERVAL_TIMEOUT_HARD_MAX) { + $timeout_max = $decoded_packet['max']; + + if ($timeout_expire > $timeout_max) { + $timeout_expire = $decoded_packet['max']; + } + } + } + + if (isset($decoded_packet['expire']) && is_int($decoded_packet['expire']) && $decoded_packet['expire'] > 0) { + if ($decoded_packet['expire'] < INTERVAL_TIMEOUT_HARD_EXPIRE && $decoded_packet['expire'] < $timeout_max) { + $timeout_expire = $decoded_packet['expire']; + } + } + + + // save basic timeout. + $stamp = strtotime('+' . $timeout_expire . ' seconds'); + $database['sessions'][$decoded_packet['ip']][$session_id]['timeouts']['expire'] = $stamp; + $database['sessions'][$decoded_packet['ip']][$session_id]['timeouts']['interval'] = $timeout_expire; + $timeouts[$stamp]['expire'][$unique_id] = $timeout; + + + // save maxiumum timeout. + $stamp = strtotime('+' . $timeout_max . ' seconds'); + $database['sessions'][$decoded_packet['ip']][$session_id]['timeouts']['max'] = $stamp; + $timeouts[$stamp]['max'][$unique_id] = $timeout; + + + // sort timeouts array for faster cleanups (this action is required). + ksort($timeouts); + + + // return the session key. + $response['result'] = array( + 'session_id' => $session_id, + 'expire' => $database['sessions'][$decoded_packet['ip']][$session_id]['timeouts']['expire'], + 'max' => $database['sessions'][$decoded_packet['ip']][$session_id]['timeouts']['max'], + 'interval' => $timeout_expire, + ); + + socket_write($client_socket, json_encode($response)); + unset($session_id); + unset($stamp); + unset($unique_id); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + else { + $response['error'] = array( + 'target' => 'no_session', + 'message' => "Either a new session_id must be requested by provided the password string or a password must be requested by providing the session id string.", + ); + + socket_write($client_socket, json_encode($response)); + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + continue; + } + + unset($response); + unset($encoded_packet); + + socket_close($client_socket); + unset($client_socket); + } while (TRUE); + + socket_close($socket); + unlink($socket_path); + unlink($pid_path); + return TRUE; +} + +/** + * Builds and returns a session id string. + * + * Based on drupal's drupal_random_bytes() + * + * @return string|false + * A base64 string. + * FALSE is returned on error. + * + * @see: https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/drupal_random_bytes/7 + */ +function build_session_id() { + $fh = @fopen('/dev/urandom', 'rb'); + if (!$fh) { + return FALSE; + } + + $bytes = fread($fh, SESSION_RANDOM_BYTES); + fclose($fh); + + return base64_encode($bytes); +} + +/** + * Searches through the database and removes expired keys. + * + * This uses a simple search design where the keys are expected to be in order. + * When the first key that represents a future time is encountered, the function will exit. + * + * @param array $database + * An array of all usernames, ips, passwords, and sessions. + * @param array $timeouts + * An array of all timeouts keyed in numeric order. + * @param string $cleartext + * Used as an unproven attempt to clear passwords from memory before delete in hopes to avoid security issues inherit in a garbage collector. + * + * @param bool + * TRUE on success, FALSE otherwise. + */ +function process_expired_sessions(&$database, &$timeouts, $cleartext) { + if (!is_array($database)) { + return FALSE; + } + + if (!is_array($timeouts)) { + return FALSE; + } + + // this is not a problem, it simply means that there is nothing to do. + if (empty($database) || empty($timeouts)) { + return TRUE; + } + + $now = strtotime('now'); + $to_delete = array(); + foreach ($timeouts as $time => &$timeout) { + if ($time > $now) { + break; + } + + // process expire timeouts. + if (!empty($timeout['expire'])) { + foreach ($timeout['expire'] as $unique_key => $setting) { + $to_delete[$unique_key] = array( + 'ip' => $setting['ip'], + 'session_id' => $setting['session_id'], + ); + } + unset($unique_key); + unset($setting); + } + + // process max timeouts. + if (!empty($timeout['max'])) { + foreach ($timeout['max'] as $unique_key => $setting) { + $to_delete[$unique_key] = array( + 'ip' => $setting['ip'], + 'session_id' => $setting['session_id'], + ); + } + unset($unique_key); + unset($setting); + } + } + unset($time); + unset($timeout); + + + // remove expired keys. + if (!empty($to_delete)) { + foreach ($to_delete as $unique_key => $setting) { + if (isset($database['session'][$setting['ip']][$setting['session_id']])) { + expire_session($database, $timeouts, $setting['session_id'], $setting['ip']); + } + } + unset($unique_key); + unset($setting); + + + // force garbage collection cleanup. + gc_collect_cycles(); + } + + return TRUE; +} + +/** + * pre-maturely expire a specific session. + * + * Use this to 'logout' of a session and remove the username and password information from this process before the session expires. + * + * @param array $database + * The database array. + * @param array $timeouts + * The timeouts array. + * @param string $session_id + * The session id string. + * @param string $ip + * The ip address string. + * + * @param bool + * TRUE on success, FALSE otherwise. + */ +function expire_session(&$database, &$timeouts, $session_id, $ip) { + if (!is_array($database) || !is_array($timeouts)) { + return FALSE; + } + + if (!is_string($session_id) || !is_string($ip)) { + return FALSE; + } + + if (!isset($database['sessions'][$ip][$session_id])) { + // if it does not exist, then consider the close successful. + return TRUE; + } + + $session = $database['session'][$ip][$session_id]; + foreach (array('expire', 'max') as $key) { + if (isset($timeouts[$session['timeouts'][$key]][$session['timeouts']['id']])) { + unset($timeouts[$session['timeouts'][$key]][$session['timeouts']['id']]); + + if (empty($timeouts[$session['timeouts'][$key]])) { + unset($timeouts[$session['timeouts'][$key]]); + } + } + } + unset($session); + unset($key); + + $database['sessions'][$ip][$session_id]['password'] = $cleartext; + unset($database['sessions'][$ip][$session_id]); + + if (empty($database['sessions'][$ip])) { + unset($database['sessions'][$ip]); + } + + return TRUE; +} + + +main($argc, $argv);