Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
# CMakeLists for servatrice directory
#
# provides the servatrice binary
project(Servatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
set(servatrice_SOURCES
src/email_parser.cpp
src/main.cpp
src/servatrice.cpp
src/servatrice_connection_pool.cpp
src/servatrice_database_interface.cpp
src/server_logger.cpp
src/serversocketinterface.cpp
src/settingscache.cpp
src/isl_interface.cpp
src/signalhandler.cpp
${VERSION_STRING_CPP}
src/smtpclient.cpp
src/smtp/qxthmac.cpp
src/smtp/qxtmailattachment.cpp
src/smtp/qxtmailmessage.cpp
src/smtp/qxtsmtp.cpp
)
set(servatrice_RESOURCES servatrice.qrc)
if(WIN32)
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
endif(WIN32)
# Under FreeBSD we need libexecinfo to use backtrace_symbols_fd()
if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD")
find_package(Libexecinfo REQUIRED)
set(SYSTEM_LIBRARIES ${EXECINFO_LIBRARY} ${SYSTEM_LIBRARIES})
endif()
if(APPLE)
set(MACOSX_BUNDLE_ICON_FILE appicon.icns)
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
set(servatrice_SOURCES ${servatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
endif(APPLE)
if(Qt6_FOUND)
qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES})
elseif(Qt5_FOUND)
qt5_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES})
endif()
set(QT_DONT_USE_QTGUI TRUE)
# Mysql connector
if(UNIX)
if(APPLE)
set(MYSQLCLIENT_DEFAULT_PATHS "/usr/local/lib" "/opt/local/lib/mysql55/mysql/" "/opt/local/lib/mysql56/mysql/")
else()
set(MYSQLCLIENT_DEFAULT_PATHS "/usr/lib64" "/usr/local/lib64" "/usr/lib" "/usr/local/lib")
endif()
elseif(WIN32)
set(MYSQLCLIENT_DEFAULT_PATHS "C:\\Program Files\\MySQL\\MySQL Server 5.7\\lib"
"C:\\Program Files (x86)\\MySQL\\MySQL Server 5.7\\lib"
)
endif()
find_library(
MYSQL_CLIENT_LIBRARIES
NAMES mysqlclient
PATHS ${MYSQLCLIENT_DEFAULT_PATHS}
PATH_SUFFIXES mysql mariadb
)
if(${MYSQL_CLIENT_LIBRARIES} MATCHES "NOTFOUND")
set(MYSQLCLIENT_FOUND
FALSE
CACHE INTERNAL ""
)
message(STATUS "MySQL connector NOT FOUND: Servatrice won't be able to connect to a MySQL server")
unset(MYSQL_CLIENT_LIBRARIES)
else()
set(MYSQLCLIENT_FOUND
TRUE
CACHE INTERNAL ""
)
get_filename_component(MYSQLCLIENT_LIBRARY_DIR ${MYSQL_CLIENT_LIBRARIES} PATH)
message(STATUS "Found MySQL connector at: ${MYSQL_CLIENT_LIBRARIES}")
endif()
# Declare path variables
set(ICONDIR
share/icons
CACHE STRING "icon dir"
)
set(DESKTOPDIR
share/applications
CACHE STRING "desktop file destination"
)
# Build servatrice binary and link it
add_executable(servatrice MACOSX_BUNDLE ${servatrice_MOC_SRCS} ${servatrice_RESOURCES_RCC} ${servatrice_SOURCES})
if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD")
target_link_libraries(
servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES}
${LIBEXECINFO_LIBRARY}
)
else()
target_link_libraries(
servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES}
)
endif()
# install rules
if(UNIX)
if(APPLE)
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.cockatrice.${PROJECT_NAME}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_NAME}-${PROJECT_VERSION}")
set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION})
set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION})
install(TARGETS servatrice BUNDLE DESTINATION ./)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.ini.example DESTINATION ./servatrice.app/Contents/Resources/)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.sql DESTINATION ./servatrice.app/Contents/Resources/)
else()
# Assume linux
install(TARGETS servatrice RUNTIME DESTINATION bin/)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.ini.example DESTINATION share/servatrice/)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.sql DESTINATION share/servatrice/)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/servatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/servatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.desktop DESTINATION ${DESKTOPDIR})
endif()
elseif(WIN32)
install(TARGETS servatrice RUNTIME DESTINATION ./)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.ini.example DESTINATION ./)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.sql DESTINATION ./)
endif()
if(APPLE)
# these needs to be relative to CMAKE_INSTALL_PREFIX
set(plugin_dest_dir servatrice.app/Contents/Plugins)
set(qtconf_dest_dir servatrice.app/Contents/Resources)
# Qt plugins: platforms, sqldrivers/mysql, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir}
COMPONENT Runtime
FILES_MATCHING
PATTERN "*.dSYM" EXCLUDE
PATTERN "*_debug.dylib" EXCLUDE
PATTERN "platforms/*.dylib"
PATTERN "sqldrivers/libqsqlmysql*.dylib"
PATTERN "tls/*.dylib"
)
install(
CODE "
file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths]
Plugins = Plugins
Translations = Resources/translations\")
"
COMPONENT Runtime
)
install(
CODE "
file(GLOB_RECURSE QTPLUGINS
\"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*.dylib\")
set(BU_CHMOD_BUNDLE_ITEMS ON)
include(BundleUtilities)
fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/servatrice.app\" \"\${QTPLUGINS}\" \"${QT_LIBRARY_DIR};${MYSQLCLIENT_LIBRARY_DIR}\")
"
COMPONENT Runtime
)
endif()
if(WIN32)
# these needs to be relative to CMAKE_INSTALL_PREFIX
set(plugin_dest_dir Plugins)
set(qtconf_dest_dir .)
install(
DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/"
DESTINATION ./
FILES_MATCHING
PATTERN "*.dll"
)
# Qt plugins: platforms, sqldrivers, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir}
COMPONENT Runtime
FILES_MATCHING
PATTERN "platforms/qdirect2d.dll"
PATTERN "platforms/qminimal.dll"
PATTERN "platforms/qoffscreen.dll"
PATTERN "platforms/qwindows.dll"
PATTERN "tls/qcertonlybackend.dll"
PATTERN "tls/qopensslbackend.dll"
PATTERN "tls/qschannelbackend.dll"
PATTERN "sqldrivers/qsqlite.dll"
PATTERN "sqldrivers/qsqlodbc.dll"
PATTERN "sqldrivers/qsqlpsql.dll"
)
install(
CODE "
file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths]
Plugins = Plugins
Translations = Resources/translations\")
"
COMPONENT Runtime
)
install(
CODE "
file(GLOB_RECURSE QTPLUGINS
\"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*.dll\")
set(BU_CHMOD_BUNDLE_ITEMS ON)
include(BundleUtilities)
fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/Servatrice.exe\" \"\${QTPLUGINS}\" \"${QT_LIBRARY_DIR};${MYSQLCLIENT_LIBRARY_DIR}\")
"
COMPONENT Runtime
)
endif()
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
set -e
version_line="$(grep 'INSERT INTO cockatrice_schema_version' servatrice/servatrice.sql)"
version_line="${version_line#*VALUES(}"
declare -i schema_ver="${version_line%%)*}"
# shellcheck disable=2012
latest_migration="$(ls -1 servatrice/migrations/ | tail -n1)"
xtoysql="${latest_migration#servatrice_}"
xtoy="${xtoysql%.sql}"
declare -i old_ver="10#${xtoy%_to_*}" #declare as integer with base 10, numbers with a leading 0 are normally interpreted as base 16
declare -i new_ver="10#${xtoy#*_to_}"
if ((old_ver >= new_ver)); then
echo "New version $new_ver is not newer than $old_ver"
exit 1
fi
if ((schema_ver != new_ver)); then
echo "Schema version $schema_ver does not equal new version $new_ver"
exit 1
fi
expected_sql="^UPDATE cockatrice_schema_version SET version=${new_ver} WHERE version=${old_ver};$"
if ! grep -q "$expected_sql" "servatrice/migrations/$latest_migration"; then
echo "$latest_migration does not contain expected sql: $expected_sql"
exit 1
fi
expected_define="^#define DATABASE_SCHEMA_VERSION $new_ver$"
if ! grep -q "$expected_define" servatrice/src/servatrice_database_interface.h; then
echo "servatrice_database_interface.h does not contain expected #define: $expected_define"
exit 1
fi
@@ -0,0 +1,7 @@
[database]
type=mysql
prefix=cockatrice
hostname=mysql
database=servatrice
user=servatrice
password=password
@@ -0,0 +1,13 @@
-- Servatrice db migration from version 0 to version 1
-- FIX #153
CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` (
`version` int(7) unsigned NOT NULL,
PRIMARY KEY (`version`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO cockatrice_schema_version VALUES(1);
-- FIX #1119
ALTER TABLE `cockatrice_rooms_gametypes` DROP PRIMARY KEY;
ALTER TABLE `cockatrice_rooms_gametypes` ADD KEY (`id_room`);
@@ -0,0 +1,8 @@
-- Servatrice db migration from version 1 to version 2
-- FIX #1281
CREATE TABLE IF NOT EXISTS `cockatrice_activation_emails` (
`name` varchar(35) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
UPDATE cockatrice_schema_version SET version=2 WHERE version=1;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 2 to version 3
alter table cockatrice_users add clientid varchar(15) not null;
UPDATE cockatrice_schema_version SET version=3 WHERE version=2;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 3 to version 4
alter table cockatrice_sessions add clientid varchar(15) not null;
UPDATE cockatrice_schema_version SET version=4 WHERE version=3;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 4 to version 5
alter table cockatrice_bans add clientid varchar(15) not null;
UPDATE cockatrice_schema_version SET version=5 WHERE version=4;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 5 to version 6
alter table cockatrice_users add last_login datetime not null;
UPDATE cockatrice_schema_version SET version=6 WHERE version=5;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 6 to version 7
alter table cockatrice_rooms add permissionlevel varchar(20) not null after descr;
UPDATE cockatrice_schema_version SET version=7 WHERE version=6;
@@ -0,0 +1,16 @@
-- Servatrice db migration from version 7 to version 8
CREATE TABLE IF NOT EXISTS `cockatrice_user_analytics` (
`id` int(7) unsigned zerofill NOT NULL,
`client_ver` varchar(35) NOT NULL,
`last_login` datetime NOT NULL,
`notes` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `cockatrice_user_analytics` (id, last_login) SELECT id, last_login FROM `cockatrice_users` WHERE last_login != '';
ALTER TABLE `cockatrice_users`
DROP COLUMN last_login;
UPDATE cockatrice_schema_version SET version=8 WHERE version=7;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 8 to version 9
alter table cockatrice_rooms add chat_history_size int(4) not null after join_message;
update cockatrice_rooms set chat_history_size = 100;
UPDATE cockatrice_schema_version SET version=9 WHERE version=8;
@@ -0,0 +1,13 @@
-- Servatrice db migration from version 9 to version 10
CREATE TABLE IF NOT EXISTS `cockatrice_warnings` (
`id` int(7) unsigned NOT NULL,
`user_name` varchar(255) NOT NULL,
`mod_name` varchar(255) NOT NULL,
`reason` text NOT NULL,
`time_of` datetime NOT NULL,
`clientid` varchar(15) NOT NULL,
PRIMARY KEY (`user_name`,`time_of`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
UPDATE cockatrice_schema_version SET version=10 WHERE version=9;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 10 to version 11
alter table cockatrice_warnings change id user_id int(7) unsigned NOT NULL;
alter table cockatrice_warnings drop primary key, add primary key(user_id,time_of);
UPDATE cockatrice_schema_version SET version=11 WHERE version=10;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 11 to version 12
alter table cockatrice_users modify token binary(16) NULL;
UPDATE cockatrice_schema_version SET version=12 WHERE version=11;
@@ -0,0 +1,86 @@
-- Servatrice db migration from version 12 to version 13
-- WARNING: this is quite a big change, so you really, REALLY should
-- backup your database before attempting to execute this migration.
-- First move all the tables to the InnoDB engine
ALTER TABLE `cockatrice_schema_version` ENGINE=InnoDB;
ALTER TABLE `cockatrice_decklist_files` ENGINE=InnoDB;
ALTER TABLE `cockatrice_decklist_folders` ENGINE=InnoDB;
ALTER TABLE `cockatrice_games` ENGINE=InnoDB;
ALTER TABLE `cockatrice_games_players` ENGINE=InnoDB;
ALTER TABLE `cockatrice_news` ENGINE=InnoDB;
ALTER TABLE `cockatrice_users` ENGINE=InnoDB;
ALTER TABLE `cockatrice_uptime` ENGINE=InnoDB;
ALTER TABLE `cockatrice_servermessages` ENGINE=InnoDB;
ALTER TABLE `cockatrice_ignorelist` ENGINE=InnoDB;
ALTER TABLE `cockatrice_buddylist` ENGINE=InnoDB;
ALTER TABLE `cockatrice_bans` ENGINE=InnoDB;
ALTER TABLE `cockatrice_warnings` ENGINE=InnoDB;
ALTER TABLE `cockatrice_sessions` ENGINE=InnoDB;
ALTER TABLE `cockatrice_servers` ENGINE=InnoDB;
ALTER TABLE `cockatrice_replays` ENGINE=InnoDB;
ALTER TABLE `cockatrice_replays_access` ENGINE=InnoDB;
ALTER TABLE `cockatrice_rooms` ENGINE=InnoDB;
ALTER TABLE `cockatrice_rooms_gametypes` ENGINE=InnoDB;
ALTER TABLE `cockatrice_log` ENGINE=InnoDB;
ALTER TABLE `cockatrice_activation_emails` ENGINE=InnoDB;
ALTER TABLE `cockatrice_user_analytics` ENGINE=InnoDB;
-- Fix the replays tables not using unsigned values for id_game and id_player
ALTER TABLE `cockatrice_replays` MODIFY COLUMN `id_game` int(7) unsigned NULL;
ALTER TABLE `cockatrice_replays_access` MODIFY COLUMN `id_game` int(7) unsigned NOT NULL;
ALTER TABLE `cockatrice_replays_access` MODIFY COLUMN `id_player` int(7) unsigned NOT NULL;
-- Now add some foreign keys between tables. Since there was no constaint before,
-- we need to ensure no leftover record (eg. a user deck without an user) exists
-- before adding the FK, or the query will fail.
DELETE FROM `cockatrice_decklist_files` WHERE `id_user` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_decklist_files` ADD FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_decklist_folders` WHERE `id_user` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_decklist_folders` ADD FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_news` WHERE `id_user` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_news` ADD FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_ignorelist` WHERE `id_user1` NOT IN (SELECT `id` FROM `cockatrice_users`);
DELETE FROM `cockatrice_ignorelist` WHERE `id_user2` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_ignorelist` ADD FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `cockatrice_ignorelist` ADD FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_buddylist` WHERE `id_user1` NOT IN (SELECT `id` FROM `cockatrice_users`);
DELETE FROM `cockatrice_buddylist` WHERE `id_user2` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_buddylist` ADD FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `cockatrice_buddylist` ADD FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_user_analytics` WHERE `id` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_user_analytics` ADD FOREIGN KEY(`id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_log` WHERE `sender_id` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_log` ADD FOREIGN KEY(`sender_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_activation_emails` WHERE `name` NOT IN (SELECT `name` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_activation_emails` ADD FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_rooms_gametypes` WHERE `id_room` NOT IN (SELECT `id` FROM `cockatrice_rooms`);
ALTER TABLE `cockatrice_rooms_gametypes` ADD FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_games_players` WHERE `id_game` NOT IN (SELECT `id` FROM `cockatrice_games`);
ALTER TABLE `cockatrice_games_players` ADD FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_replays` WHERE `id_game` NOT IN (SELECT `id` FROM `cockatrice_games`);
ALTER TABLE `cockatrice_replays` ADD FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_replays_access` WHERE `id_game` NOT IN (SELECT `id` FROM `cockatrice_games`);
ALTER TABLE `cockatrice_replays_access` ADD FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_replays_access` WHERE `id_player` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_replays_access` ADD FOREIGN KEY(`id_player`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
DELETE FROM `cockatrice_bans` WHERE `id_admin` NOT IN (SELECT `id` FROM `cockatrice_users`);
ALTER TABLE `cockatrice_bans` ADD FOREIGN KEY(`id_admin`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- Last: update schema version
UPDATE cockatrice_schema_version SET version=13 WHERE version=12;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 13 to version 14
alter table cockatrice_sessions add `connection_type` ENUM('tcp', 'websocket');
UPDATE cockatrice_sessions SET connection_type = 'tcp';
UPDATE cockatrice_schema_version SET version=14 WHERE version=13;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 14 to version 15
alter table cockatrice_rooms add `id_server` tinyint(3) not null default 0;
alter table cockatrice_rooms_gametypes add `id_server` tinyint(3) not null default 0;
UPDATE cockatrice_schema_version SET version=15 WHERE version=14;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 15 to version 16
drop table cockatrice_news;
UPDATE cockatrice_schema_version SET version=16 WHERE version=15;
@@ -0,0 +1,7 @@
-- Servatrice db migration from version 16 to version 17
alter table cockatrice_rooms modify column `id_server` tinyint(3) not null default 1;
alter table cockatrice_rooms_gametypes modify column `id_server` tinyint(3) not null default 1;
alter table cockatrice_servermessages modify column `id_server` tinyint(3) not null default 1;
UPDATE cockatrice_schema_version SET version=17 WHERE version=16;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 17 to version 18
alter table cockatrice_users add column privlevel enum("NONE","VIP","DONATOR") NOT NULL;
UPDATE cockatrice_schema_version SET version=18 WHERE version=17;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 18 to version 19
alter table cockatrice_sessions modify column `user_name` varchar(35) NOT NULL;
alter table cockatrice_sessions modify column `ip_address` varchar(255) NOT NULL;
UPDATE cockatrice_schema_version SET version=19 WHERE version=18;
@@ -0,0 +1,20 @@
-- Servatrice db migration from version 19 to version 20
alter table cockatrice_users add column privlevelStartDate datetime NOT NULL;
alter table cockatrice_users add column privlevelEndDate datetime NOT NULL;
update cockatrice_users set privlevelStartDate = NOW() where privlevel != 'NONE';
update cockatrice_users set privlevelEndDate = DATE_ADD(NOW() , INTERVAL 30 DAY) where privlevel != 'NONE';
CREATE TABLE IF NOT EXISTS `cockatrice_donations` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`payment_pre_fee` double DEFAULT NULL,
`payment_post_fee` double DEFAULT NULL,
`term_length` int(11) DEFAULT NULL,
`date` varchar(255) DEFAULT NULL,
`pp_type` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
UPDATE cockatrice_schema_version SET version=20 WHERE version=19;
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS `cockatrice_forgot_password` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`name` varchar(35) NOT NULL,
`requestDate` datetime NOT NULL default '0000-00-00 00:00:00',
`emailed` tinyint(1) NOT NULL default 0,
PRIMARY KEY (`id`),
KEY `user_name` (`name`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
UPDATE cockatrice_schema_version SET version=21 WHERE version=20;
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS `cockatrice_audit` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`id_server` tinyint(3) NOT NULL,
`name` varchar(35) NOT NULL,
`ip_address` varchar(255) NOT NULL,
`clientid` varchar(15) NOT NULL,
`incidentDate` datetime NOT NULL default '0000-00-00 00:00:00',
`action` varchar(35) NOT NULL,
`results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail',
`details` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_name` (`name`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
UPDATE cockatrice_schema_version SET version=22 WHERE version=21;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 22 to version 23
alter table cockatrice_rooms modify column permissionlevel enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR');
alter table cockatrice_rooms add column privlevel enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL;
UPDATE cockatrice_schema_version SET version=23 WHERE version=22;
@@ -0,0 +1,62 @@
-- Servatrice db migration from version 23 to version 24
SET FOREIGN_KEY_CHECKS=0;
-- short the "ip address" columns to 45 chars (max length of an ipv6 address)
-- to ensure the field can be used as a key on mysql < 5.7
-- (not all fields are actually keys, but better keep them uniform)
ALTER TABLE `cockatrice_sessions` MODIFY COLUMN `ip_address` varchar(45) NOT NULL;
ALTER TABLE `cockatrice_bans` MODIFY COLUMN `ip_address` varchar(45) NOT NULL;
ALTER TABLE `cockatrice_log` MODIFY COLUMN `sender_ip` varchar(45) NOT NULL;
ALTER TABLE `cockatrice_audit` MODIFY COLUMN `ip_address` varchar(45) NOT NULL;
-- short the "user name" columns to 35 chars (current max length in servatrice)
-- to ensure the field can be used as a key on mysql < 5.7
-- (not all fields are actually keys, but better keep them uniform)
ALTER TABLE `cockatrice_bans` MODIFY COLUMN `user_name` varchar(35) NOT NULL;
ALTER TABLE `cockatrice_warnings` MODIFY COLUMN `user_name` varchar(35) NOT NULL;
ALTER TABLE `cockatrice_warnings` MODIFY COLUMN `mod_name` varchar(35) NOT NULL;
ALTER TABLE `cockatrice_games` MODIFY COLUMN `creator_name` varchar(35) NOT NULL;
ALTER TABLE `cockatrice_games_players` MODIFY COLUMN `player_name` varchar(35) NOT NULL;
ALTER TABLE `cockatrice_donations` MODIFY COLUMN `username` varchar(35) NOT NULL;
-- remove the FK from cockatrice_activation_emails (it will be created again later)
-- the key name should end with _1, but multiple run of the 0012_to_003 migration
-- can lead to a different name or even multiple keys. In this case you must remove
-- all of them before continue.
-- Use "show create table cockatrice_activation_emails" to see the key names.
ALTER TABLE cockatrice_activation_emails DROP FOREIGN KEY `cockatrice_activation_emails_ibfk_1`;
-- unify tables and columns collation to utf8mb4_unicode_ci
ALTER TABLE `cockatrice_schema_version` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_decklist_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_decklist_folders` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_ignorelist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_buddylist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_rooms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_rooms_gametypes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_games` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_games_players` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_replays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_replays_access` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_servers` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_uptime` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_servermessages` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_sessions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_bans` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_warnings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_log` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_activation_emails` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_user_analytics` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_donations` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_forgot_password` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `cockatrice_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- re-add the FK constraint on cockatrice_activation_emails
ALTER TABLE `cockatrice_activation_emails` ADD FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`) ON DELETE CASCADE ON UPDATE CASCADE;
SET FOREIGN_KEY_CHECKS=1;
-- update schema version
UPDATE cockatrice_schema_version SET version=24 WHERE version=23;
@@ -0,0 +1,6 @@
-- Servatrice db migration from version 24 to version 25
ALTER TABLE cockatrice_uptime ADD COLUMN mods_count int(11) NOT NULL DEFAULT 0;
ALTER TABLE cockatrice_uptime ADD COLUMN mods_list TEXT;
UPDATE cockatrice_schema_version SET version=25 WHERE version=24;
@@ -0,0 +1,171 @@
-- Servatrice db migration from version 25 to version 26
-- Some previous migrations didn't care about column ordering,
-- meaning select could return info in a different order depending on the
-- age of the database.
-- This migration ensures a consistent column ordering across all databases,
-- regardless of age. Future migrations should take care to ensure this
-- ordering stays consistent.
-- cockatrice_users.id cannot be modified because its used in a foreign key constraint
-- it should be first anyway, so this isnt a big deal
-- ALTER TABLE cockatrice_users MODIFY `id` int(7) unsigned zerofill NOT NULL FIRST;
ALTER TABLE cockatrice_users MODIFY COLUMN `admin` tinyint(1) NOT NULL AFTER `id`;
ALTER TABLE cockatrice_users MODIFY COLUMN `name` varchar(35) NOT NULL AFTER `admin`;
ALTER TABLE cockatrice_users MODIFY COLUMN `realname` varchar(255) NOT NULL AFTER `name`;
ALTER TABLE cockatrice_users MODIFY COLUMN `gender` char(1) NOT NULL AFTER `realname`;
ALTER TABLE cockatrice_users MODIFY COLUMN `password_sha512` char(120) NOT NULL AFTER `gender`;
ALTER TABLE cockatrice_users MODIFY COLUMN `email` varchar(255) NOT NULL AFTER `password_sha512`;
ALTER TABLE cockatrice_users MODIFY COLUMN `country` char(2) NOT NULL AFTER `email`;
ALTER TABLE cockatrice_users MODIFY COLUMN `avatar_bmp` blob NOT NULL AFTER `country`;
ALTER TABLE cockatrice_users MODIFY COLUMN `registrationDate` datetime NOT NULL AFTER `avatar_bmp`;
ALTER TABLE cockatrice_users MODIFY COLUMN `active` tinyint(1) NOT NULL AFTER `registrationDate`;
ALTER TABLE cockatrice_users MODIFY COLUMN `token` binary(16) AFTER `active`;
ALTER TABLE cockatrice_users MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `token`;
ALTER TABLE cockatrice_users MODIFY COLUMN `privlevel` enum("NONE","VIP","DONATOR") NOT NULL AFTER `clientid`;
ALTER TABLE cockatrice_users MODIFY COLUMN `privlevelStartDate` datetime NOT NULL AFTER `privlevel`;
ALTER TABLE cockatrice_users MODIFY COLUMN `privlevelEndDate` datetime NOT NULL AFTER `privlevelStartDate`;
ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST;
ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `id_folder` int(7) unsigned zerofill NOT NULL AFTER `id`;
ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `id_user` int(7) unsigned NULL AFTER `id_folder`;
ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `name` varchar(50) NOT NULL AFTER `id_user`;
ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `upload_time` datetime NOT NULL AFTER `name`;
ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `content` text NOT NULL AFTER `upload_time`;
ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST;
ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `id_parent` int(7) unsigned zerofill NOT NULL AFTER `id`;
ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `id_user` int(7) unsigned NULL AFTER `id_parent`;
ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `name` varchar(30) NOT NULL AFTER `id_user`;
ALTER TABLE cockatrice_ignorelist MODIFY COLUMN `id_user1` int(7) unsigned NOT NULL FIRST;
ALTER TABLE cockatrice_ignorelist MODIFY COLUMN `id_user2` int(7) unsigned NOT NULL AFTER `id_user1`;
ALTER TABLE cockatrice_buddylist MODIFY COLUMN `id_user1` int(7) unsigned NOT NULL FIRST;
ALTER TABLE cockatrice_buddylist MODIFY COLUMN `id_user2` int(7) unsigned NOT NULL AFTER `id_user1`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `id` int(7) unsigned NOT NULL auto_increment FIRST;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `name` varchar(50) NOT NULL AFTER `id`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `descr` varchar(255) NOT NULL AFTER `name`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `permissionlevel` enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR') NOT NULL AFTER `descr`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `privlevel` enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL AFTER `permissionlevel`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `auto_join` tinyint(1) default 0 AFTER `privlevel`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `join_message` varchar(255) NOT NULL AFTER `auto_join`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `chat_history_size` int(4) NOT NULL AFTER `join_message`;
ALTER TABLE cockatrice_rooms MODIFY COLUMN `id_server` tinyint(3) NOT NULL DEFAULT 1 AFTER `chat_history_size`;
ALTER TABLE cockatrice_rooms_gametypes MODIFY COLUMN `id_room` int(7) unsigned NOT NULL FIRST;
ALTER TABLE cockatrice_rooms_gametypes MODIFY COLUMN `name` varchar(50) NOT NULL AFTER `id_room`;
ALTER TABLE cockatrice_rooms_gametypes MODIFY COLUMN `id_server` tinyint(3) NOT NULL DEFAULT 1 AFTER `name`;
ALTER TABLE cockatrice_games MODIFY COLUMN `room_name` varchar(255) NOT NULL FIRST;
ALTER TABLE cockatrice_games MODIFY COLUMN `id` int(7) unsigned NOT NULL auto_increment AFTER `room_name`;
ALTER TABLE cockatrice_games MODIFY COLUMN `descr` varchar(50) default NULL AFTER `id`;
ALTER TABLE cockatrice_games MODIFY COLUMN `creator_name` varchar(35) NOT NULL AFTER `descr`;
ALTER TABLE cockatrice_games MODIFY COLUMN `password` tinyint(1) NOT NULL AFTER `creator_name`;
ALTER TABLE cockatrice_games MODIFY COLUMN `game_types` varchar(255) NOT NULL AFTER `password`;
ALTER TABLE cockatrice_games MODIFY COLUMN `player_count` tinyint(3) NOT NULL AFTER `game_types`;
ALTER TABLE cockatrice_games MODIFY COLUMN `time_started` datetime default NULL AFTER `player_count`;
ALTER TABLE cockatrice_games MODIFY COLUMN `time_finished` datetime default NULL AFTER `time_started`;
ALTER TABLE cockatrice_games_players MODIFY COLUMN `id_game` int(7) unsigned zerofill NOT NULL FIRST;
ALTER TABLE cockatrice_games_players MODIFY COLUMN `player_name` varchar(35) NOT NULL AFTER `id_game`;
ALTER TABLE cockatrice_replays MODIFY COLUMN `id` int(7) NOT NULL AUTO_INCREMENT FIRST;
ALTER TABLE cockatrice_replays MODIFY COLUMN `id_game` int(7) unsigned NULL AFTER `id`;
ALTER TABLE cockatrice_replays MODIFY COLUMN `duration` int(7) NOT NULL AFTER `id_game`;
ALTER TABLE cockatrice_replays MODIFY COLUMN `replay` mediumblob NOT NULL AFTER `duration`;
ALTER TABLE cockatrice_replays_access MODIFY COLUMN `id_game` int(7) unsigned NOT NULL FIRST;
ALTER TABLE cockatrice_replays_access MODIFY COLUMN `id_player` int(7) unsigned NOT NULL AFTER `id_game`;
ALTER TABLE cockatrice_replays_access MODIFY COLUMN `replay_name` varchar(255) NOT NULL AFTER `id_player`;
ALTER TABLE cockatrice_replays_access MODIFY COLUMN `do_not_hide` tinyint(1) NOT NULL AFTER `replay_name`;
ALTER TABLE cockatrice_servers MODIFY COLUMN `id` mediumint(8) unsigned NOT NULL FIRST;
ALTER TABLE cockatrice_servers MODIFY COLUMN `ssl_cert` text NOT NULL AFTER `id`;
ALTER TABLE cockatrice_servers MODIFY COLUMN `hostname` varchar(255) NOT NULL AFTER `ssl_cert`;
ALTER TABLE cockatrice_servers MODIFY COLUMN `address` varchar(255) NOT NULL AFTER `hostname`;
ALTER TABLE cockatrice_servers MODIFY COLUMN `game_port` mediumint(8) unsigned NOT NULL AFTER `address`;
ALTER TABLE cockatrice_servers MODIFY COLUMN `control_port` mediumint(9) NOT NULL AFTER `game_port`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `id_server` tinyint(3) NOT NULL FIRST;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `timest` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER `id_server`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `uptime` int(11) NOT NULL AFTER `timest`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `users_count` int(11) NOT NULL AFTER `uptime`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `mods_count` int(11) NOT NULL DEFAULT 0 AFTER `users_count`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `mods_list` TEXT AFTER `mods_count`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `games_count` int(11) NOT NULL AFTER `mods_list`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `rx_bytes` int(11) NOT NULL AFTER `games_count`;
ALTER TABLE cockatrice_uptime MODIFY COLUMN `tx_bytes` int(11) NOT NULL AFTER `rx_bytes`;
ALTER TABLE cockatrice_servermessages MODIFY COLUMN `id_server` tinyint(3) not null default 1 FIRST;
ALTER TABLE cockatrice_servermessages MODIFY COLUMN `timest` datetime NOT NULL default '0000-00-00 00:00:00' AFTER `id_server`;
ALTER TABLE cockatrice_servermessages MODIFY COLUMN `message` text AFTER `timest`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `id` int(9) NOT NULL AUTO_INCREMENT FIRST;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `user_name` varchar(35) NOT NULL AFTER `id`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `id_server` tinyint(3) NOT NULL AFTER `user_name`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `ip_address` varchar(45) NOT NULL AFTER `id_server`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `start_time` datetime NOT NULL AFTER `ip_address`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `end_time` datetime DEFAULT NULL AFTER `start_time`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `end_time`;
ALTER TABLE cockatrice_sessions MODIFY COLUMN `connection_type` ENUM('tcp', 'websocket') AFTER `clientid`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `user_name` varchar(35) NOT NULL FIRST;
ALTER TABLE cockatrice_bans MODIFY COLUMN `ip_address` varchar(45) NOT NULL AFTER `user_name`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `id_admin` int(7) unsigned zerofill NOT NULL AFTER `ip_address`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `time_from` datetime NOT NULL AFTER `id_admin`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `minutes` int(6) NOT NULL AFTER `time_from`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `reason` text NOT NULL AFTER `minutes`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `visible_reason` text NOT NULL AFTER `reason`;
ALTER TABLE cockatrice_bans MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `visible_reason`;
ALTER TABLE cockatrice_warnings MODIFY COLUMN `user_id` int(7) unsigned NOT NULL FIRST;
ALTER TABLE cockatrice_warnings MODIFY COLUMN `user_name` varchar(35) NOT NULL AFTER `user_id`;
ALTER TABLE cockatrice_warnings MODIFY COLUMN `mod_name` varchar(35) NOT NULL AFTER `user_name`;
ALTER TABLE cockatrice_warnings MODIFY COLUMN `reason` text NOT NULL AFTER `mod_name`;
ALTER TABLE cockatrice_warnings MODIFY COLUMN `time_of` datetime NOT NULL AFTER `reason`;
ALTER TABLE cockatrice_warnings MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `time_of`;
ALTER TABLE cockatrice_log MODIFY COLUMN `log_time` datetime NOT NULL FIRST;
ALTER TABLE cockatrice_log MODIFY COLUMN `sender_id` int(7) unsigned NULL AFTER `log_time`;
ALTER TABLE cockatrice_log MODIFY COLUMN `sender_name` varchar(35) NOT NULL AFTER `sender_id`;
ALTER TABLE cockatrice_log MODIFY COLUMN `sender_ip` varchar(45) NOT NULL AFTER `sender_name`;
ALTER TABLE cockatrice_log MODIFY COLUMN `log_message` text NOT NULL AFTER `sender_ip`;
ALTER TABLE cockatrice_log MODIFY COLUMN `target_type` ENUM('room', 'game', 'chat') AFTER `log_message`;
ALTER TABLE cockatrice_log MODIFY COLUMN `target_id` int(7) NULL AFTER `target_type`;
ALTER TABLE cockatrice_log MODIFY COLUMN `target_name` varchar(50) NOT NULL AFTER `target_id`;
-- cockatrice_activation_emails has only 1 column so we skip it
ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL FIRST;
ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `client_ver` varchar(35) NOT NULL AFTER `id`;
ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `last_login` datetime NOT NULL AFTER `client_ver`;
ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `notes` varchar(255) NOT NULL AFTER `last_login`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `id` int(11) unsigned NOT NULL AUTO_INCREMENT FIRST;
ALTER TABLE cockatrice_donations MODIFY COLUMN `username` varchar(35) DEFAULT NULL AFTER `id`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `email` varchar(255) DEFAULT NULL AFTER `username`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `payment_pre_fee` double DEFAULT NULL AFTER `email`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `payment_post_fee` double DEFAULT NULL AFTER `payment_pre_fee`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `term_length` int(11) DEFAULT NULL AFTER `payment_post_fee`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `date` varchar(255) DEFAULT NULL AFTER `term_length`;
ALTER TABLE cockatrice_donations MODIFY COLUMN `pp_type` varchar(255) DEFAULT NULL AFTER `date`;
ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST;
ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `name` varchar(35) NOT NULL AFTER `id`;
ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `requestDate` datetime NOT NULL default '0000-00-00 00:00:00' AFTER `name`;
ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `emailed` tinyint(1) NOT NULL default 0 AFTER `requestDate`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST;
ALTER TABLE cockatrice_audit MODIFY COLUMN `id_server` tinyint(3) NOT NULL AFTER `id`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `name` varchar(35) NOT NULL AFTER `id_server`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `ip_address` varchar(45) NOT NULL AFTER `name`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `ip_address`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `incidentDate` datetime NOT NULL default '0000-00-00 00:00:00' AFTER `clientid`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `action` varchar(35) NOT NULL AFTER `incidentDate`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail' AFTER `action`;
ALTER TABLE cockatrice_audit MODIFY COLUMN `details` varchar(255) NOT NULL AFTER `results`;
UPDATE cockatrice_schema_version SET version=26 WHERE version=25;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 26 to version 27
ALTER TABLE cockatrice_users ADD COLUMN passwordLastChangedDate datetime NOT NULL DEFAULT '0000-00-00 00:00:00';
UPDATE cockatrice_schema_version SET version=27 WHERE version=26;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 27 to version 28
ALTER TABLE cockatrice_users DROP COLUMN gender;
UPDATE cockatrice_schema_version SET version=28 WHERE version=27;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 28 to version 29
ALTER TABLE cockatrice_users MODIFY COLUMN avatar_bmp mediumblob NOT NULL;
UPDATE cockatrice_schema_version SET version=29 WHERE version=28;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 29 to version 30
ALTER TABLE cockatrice_users ADD COLUMN adminnotes mediumtext NOT NULL;
UPDATE cockatrice_schema_version SET version=30 WHERE version=29;
@@ -0,0 +1,11 @@
-- Servatrice db migration from version 30 to version 31
ALTER TABLE cockatrice_log DROP INDEX `target_type`;
ALTER TABLE cockatrice_forgot_password ADD INDEX idx_emailed (`emailed`);
ALTER TABLE cockatrice_sessions ADD INDEX idx_start_time (`start_time`);
ALTER TABLE cockatrice_users ADD INDEX idx_admin (`admin`);
ALTER TABLE cockatrice_users ADD INDEX idx_active (`active`);
ALTER TABLE cockatrice_users ADD INDEX idx_privlevel (`privlevel`);
UPDATE cockatrice_schema_version SET version=31 WHERE version=30;
@@ -0,0 +1,11 @@
-- Servatrice db migration from version 31 to version 32
ALTER TABLE cockatrice_users ADD INDEX `idx_clientid` (`clientid`);
ALTER TABLE cockatrice_sessions ADD INDEX `idx_clientid` (`clientid`);
ALTER TABLE cockatrice_sessions ADD INDEX `idx_ip_address` (`ip_address`);
ALTER TABLE cockatrice_bans ADD INDEX `idx_user_name` (`user_name`);
ALTER TABLE cockatrice_warnings ADD INDEX `idx_time_of` (`time_of`);
ALTER TABLE cockatrice_warnings ADD INDEX `idx_user_name` (`user_name`);
ALTER TABLE cockatrice_log ADD INDEX `idx_log_time` (`log_time`);
UPDATE cockatrice_schema_version SET version=32 WHERE version=31;
@@ -0,0 +1,5 @@
-- Servatrice db migration from version 32 to version 33
ALTER TABLE cockatrice_user_analytics ADD INDEX `idx_last_login` (`last_login`);
UPDATE cockatrice_schema_version SET version=33 WHERE version=32;
@@ -0,0 +1,8 @@
-- Servatrice db migration from version 33 to version 34
ALTER TABLE cockatrice_users ADD COLUMN leftPawnColorOverride varchar(255);
ALTER TABLE cockatrice_users ADD COLUMN rightPawnColorOverride varchar(255);
ALTER TABLE cockatrice_users ADD INDEX `idx_pawnColorOverrides` (`leftPawnColorOverride`, `rightPawnColorOverride`);
UPDATE cockatrice_schema_version SET version=34 WHERE version=33;
@@ -0,0 +1,19 @@
ALTER TABLE `cockatrice_users` ADD COLUMN `card_art_params` TEXT DEFAULT NULL, ALGORITHM=INSTANT;
CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`card_name` varchar(255) NOT NULL,
`card_provider_id` varchar(255) NOT NULL,
`mode` enum('ALLOW','DENY') NOT NULL,
`reason` varchar(255) DEFAULT NULL,
`created_by` int(7) unsigned DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`),
KEY `idx_mode` (`mode`),
FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
UPDATE cockatrice_schema_version SET version=35 WHERE version=34;
+4
View File
@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

+1
View File
@@ -0,0 +1 @@
pypb/
Binary file not shown.
@@ -0,0 +1,5 @@
# Local state - never commit these
state.json
state.json.tmp
venv/
__pycache__/
@@ -0,0 +1,163 @@
# Account registration monitor
Posts a Discord message whenever a new account is registered in Servatrice
(`cockatrice_users`). Each message includes the username, real name (if set),
email, and registration time.
It runs as a periodic read-only query against the production database. It does
not modify the database and does not touch the running Servatrice process.
## How it decides what is "new"
Accounts get an auto-increment `id`, so "new since last time" is just
`id > last_seen_id`. The monitor stores that single high-water-mark id in its
state file. Each run it posts every account above the mark, oldest first, then
advances the mark to the highest id it posted.
Because the mark only moves forward and an id is posted exactly once, there are
no duplicate messages and nothing is missed, even if the monitor is down for a
while. The state file holds a single number, so it never grows.
The first run (when no state file exists yet) records the current maximum id as
the baseline and posts nothing. This prevents the entire existing user base from
being dumped into the channel. Only accounts registered after that baseline are
posted.
If a post to Discord fails, the monitor stops there without advancing the mark
past it, so that account and everything after it are retried on the next run.
## Privacy note
Messages contain personal data (real name and email). Discord stores message
content on their servers, so post only to a private channel that the right
people can see, and treat the webhook URL as a secret. It lives in the config
ini alongside the database password, so keep that file readable only by the user
that runs the monitor.
## Setup
The monitor reads its database credentials and the webhook from a
servatrice-style ini file passed with `--config` (or the `CONFIG_FILE` env var).
You can point it at your existing `servatrice.ini`, or keep a small separate ini
just for the monitor.
### 1. Create a read-only database user
Run as a DB admin. Adjust the host (`'%'` allows any host; restrict it to the
machine running the monitor if you can) and the table prefix if yours is not the
default `cockatrice`.
```sql
CREATE USER 'account_monitor'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT (id, name, realname, email, registrationDate)
ON servatrice.cockatrice_users TO 'account_monitor'@'%';
FLUSH PRIVILEGES;
```
Using a read-only user is recommended over pointing `--config` at the real
`servatrice.ini`, because Servatrice's own DB account usually has write access
the monitor does not need.
### 2. Create the Discord webhook and add it to the config
In Discord: open the target channel, then Edit Channel -> Integrations ->
Webhooks -> New Webhook. Name it, pick the channel, and copy the webhook URL.
Add a `[discord]` section with the URL to the ini you will pass to `--config`.
If you want the read-only user above, set the `[database]` section to use it. A
small dedicated `monitor.ini` looks like this:
```ini
[database]
hostname=127.0.0.1
database=servatrice
user=account_monitor
password=a-strong-password
prefix=cockatrice
[discord]
new_user_activation_webhook=https://discord.com/api/webhooks/XXXX/YYYY
```
If you would rather use one file, add the `[discord]` section to the real
`servatrice.ini` instead. Servatrice ignores sections it does not use. Note that
Servatrice (a Qt app) rewrites ini values it touches in quoted, backslash-escaped
form, for example `"https\://..."`. The monitor strips that encoding from the
webhook automatically, so either the plain or the escaped form works.
### 3. Install
```bash
cd servatrice/scripts/account_monitor
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
```
### 4. Verify before scheduling
```bash
# Confirm the webhook works (sends one test message to the channel)
./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --test-webhook
# Confirm DB access and see what it would do, without posting or writing state
./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --dry-run --verbose
```
The first real run seeds the baseline and posts nothing:
```bash
./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini
```
After that, test it end to end by registering a throwaway account and confirming
a message appears on the next run.
## Run it every 2 minutes with cron
Edit the crontab of the user that owns the script directory (`crontab -e`) and
add one line. This runs the monitor every 2 minutes, using the venv's Python and
your config ini, and appends output to a log:
```cron
*/2 * * * * cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1
```
Adjust the three paths to your install: the script directory after `cd`, and the
`--config` and log paths. The `*/2` field is what makes it run every 2 minutes;
change it to `*/5` for every 5, and so on.
By default the high-water-mark is stored in `state.json` next to the script, so
the directory must be writable by the cron user. To put it elsewhere, set
`STATE_FILE`:
```cron
*/2 * * * * STATE_FILE=/var/lib/account_monitor/state.json cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1
```
The interval only controls how often it checks; it is not a lookback window, so
a longer interval never causes missed accounts. The query is cheap: an indexed
range scan on the primary key for `id > last_seen`.
## Options
- `--config PATH` / `-c PATH` — read DB settings from `[database]` and the webhook from `[discord] new_user_activation_webhook` of a servatrice-style ini (falls back to the `CONFIG_FILE` env var).
- `--dry-run` — query and log what would be posted; no Discord posts, no state write.
- `--test-webhook` — send one test message to the webhook and exit (does not need DB credentials).
- `--verbose` — debug logging.
## Configuration reference
Settings come from the `--config` ini, with environment variables available as
overrides if you need them (env takes precedence over the ini).
| Setting | ini (`--config`) | Environment override |
| --- | --- | --- |
| DB host | `[database] hostname` | `DB_HOST` |
| DB port | `[database] port` (optional) | `DB_PORT` |
| DB name | `[database] database` | `DB_NAME` |
| DB user | `[database] user` | `DB_USER` |
| DB password | `[database] password` | `DB_PASSWORD` |
| Table prefix | `[database] prefix` | `DB_TABLE_PREFIX` |
| Webhook URL | `[discord] new_user_activation_webhook` | `DISCORD_WEBHOOK_URL` |
| DB TLS | — | `DB_SSL` / `DB_SSL_CA` |
| State file path | — | `STATE_FILE` (default: `state.json` next to the script) |
+350
View File
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""Post a Discord message when a new Servatrice account is registered.
Accounts get an auto-increment `id`, so "what is new since last time" is simply
`id > last_seen_id`. The monitor stores that single high-water-mark id in a
small state file. Each run it posts every account above the mark (oldest
first), then advances the mark. This means no duplicate posts, nothing missed
across downtime, and a state file that never grows (it holds one number).
On the very first run (no state file yet) it records the current maximum id as
the baseline and posts nothing, so existing users are not dumped into the
channel. From then on only newly-registered accounts are posted.
Intended to be run on a schedule (cron). Pass a servatrice-style ini with
--config (or CONFIG_FILE) for the database credentials and webhook; see
README.md.
"""
import argparse
import configparser
import json
import logging
import os
import re
import sys
import time
import urllib.error
import urllib.request
import pymysql
log = logging.getLogger("account_monitor")
# Columns we use. `id` drives the high-water-mark; `name` is the login/username,
# `realname` is the optional display name, `registrationDate` is when the account
# row was created.
NEW_ACCOUNTS_QUERY = (
"SELECT id, name, realname, email, registrationDate "
"FROM `{prefix}_users` WHERE id > %s ORDER BY id ASC"
)
EMBED_COLOR = 0x5865F2 # discord blurple
DISCORD_MAX_EMBED_FIELD = 1024
POST_DELAY_SECONDS = 1.0 # gap between webhook posts to stay under rate limits
MAX_RATELIMIT_RETRIES = 5
def _clean_ini_value(value):
"""Undo Qt QSettings ini encoding of a value.
Qt apps (including Servatrice) write ini values that contain special
characters wrapped in double quotes and backslash-escaped, e.g. a webhook
URL stored as "https\\://...". configparser returns that text literally, so
strip the wrapping quotes and remove the backslash escapes. This is applied
to the webhook URL only, where it is safe (URLs contain no quotes or
backslashes); DB values are left untouched so passwords are never altered.
"""
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
return re.sub(r"\\(.)", r"\1", value)
def load_config_file(path):
"""Read DB and Discord settings from a servatrice-style ini.
Pulls the [database] section (hostname/database/user/password/prefix/port)
and the Discord webhook from [discord] new_user_activation_webhook. Returns
a dict using this module's internal config keys; only keys actually present
(and non-empty) in the file are returned, so missing values fall back to
defaults or the environment.
"""
# interpolation=None so a '%' in a password is not treated as a token.
parser = configparser.ConfigParser(interpolation=None)
if not parser.read(path):
log.error("Config file not found or unreadable: %s", path)
sys.exit(2)
result = {}
if parser.has_section("database"):
db = parser["database"]
db_mapping = {
"hostname": "db_host",
"database": "db_name",
"user": "db_user",
"password": "db_password",
"prefix": "db_prefix",
"port": "db_port", # not in stock servatrice.ini, but honored if present
}
result.update({cfg_key: db[ini_key] for ini_key, cfg_key in db_mapping.items() if db.get(ini_key)})
if parser.has_section("discord") and parser["discord"].get("new_user_activation_webhook"):
result["webhook_url"] = _clean_ini_value(parser["discord"]["new_user_activation_webhook"])
return result
def get_config(config_path=None, require_db=True):
"""Build configuration from defaults, an optional ini file, then env vars.
Precedence, highest first: environment variables, the ini file, built-in
defaults. Database credentials and the Discord webhook may come from either
the ini file or the environment; the state file is environment-only.
"""
cfg = {
"db_host": "localhost",
"db_port": 3306,
"db_name": "servatrice",
"db_user": None,
"db_password": None,
"db_prefix": "cockatrice",
"db_ssl": False,
"db_ssl_ca": None,
"webhook_url": None,
"state_file": os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"),
}
if config_path:
cfg.update(load_config_file(config_path))
env_map = {
"DB_HOST": "db_host",
"DB_PORT": "db_port",
"DB_NAME": "db_name",
"DB_USER": "db_user",
"DB_PASSWORD": "db_password",
"DB_TABLE_PREFIX": "db_prefix",
"DB_SSL_CA": "db_ssl_ca",
"DISCORD_WEBHOOK_URL": "webhook_url",
"STATE_FILE": "state_file",
}
for env_key, cfg_key in env_map.items():
if os.environ.get(env_key):
cfg[cfg_key] = os.environ[env_key]
if os.environ.get("DB_SSL"):
cfg["db_ssl"] = os.environ["DB_SSL"].lower() in ("1", "true", "yes")
cfg["db_port"] = int(cfg["db_port"])
required = {"webhook_url": "DISCORD_WEBHOOK_URL or [discord] new_user_activation_webhook"}
if require_db:
required["db_user"] = "DB_USER or [database] user"
required["db_password"] = "DB_PASSWORD or [database] password"
missing = [label for key, label in required.items() if not cfg[key]]
if missing:
log.error("Missing required configuration: %s", "; ".join(missing))
sys.exit(2)
if cfg["webhook_url"] and not cfg["webhook_url"].lower().startswith(("http://", "https://")):
log.error("Webhook URL does not look like an http(s) URL: %r", cfg["webhook_url"])
sys.exit(2)
return cfg
def connect(cfg):
"""Open a read-only connection to the Servatrice database."""
ssl = None
if cfg["db_ssl"]:
ssl = {"ca": cfg["db_ssl_ca"]} if cfg["db_ssl_ca"] else {}
return pymysql.connect(
host=cfg["db_host"],
port=cfg["db_port"],
user=cfg["db_user"],
password=cfg["db_password"],
database=cfg["db_name"],
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
connect_timeout=15,
read_timeout=30,
ssl=ssl,
)
def fetch_max_id(conn, prefix):
"""Return the highest account id currently in the table, or 0 if empty."""
with conn.cursor() as cur:
cur.execute("SELECT MAX(id) AS max_id FROM `{prefix}_users`".format(prefix=prefix))
row = cur.fetchone()
return int(row["max_id"]) if row and row["max_id"] is not None else 0
def fetch_new_accounts(conn, prefix, last_id):
"""Return detail rows for accounts with id > last_id, oldest first."""
with conn.cursor() as cur:
cur.execute(NEW_ACCOUNTS_QUERY.format(prefix=prefix), (last_id,))
return cur.fetchall()
def load_state(path):
"""Load the high-water-mark id. Returns (last_id, is_first_run)."""
if not os.path.exists(path):
return 0, True
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
return int(data.get("last_id", 0)), False
def save_state(path, last_id):
"""Atomically persist the high-water-mark id."""
directory = os.path.dirname(path)
if directory:
os.makedirs(directory, exist_ok=True)
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump({"version": 2, "last_id": int(last_id)}, fh)
os.replace(tmp, path)
def build_embed(row):
"""Build a Discord embed dict for one newly-registered account."""
fields = [
{"name": "Username", "value": str(row["name"]) or "(none)", "inline": False},
]
realname = (row.get("realname") or "").strip()
if realname:
fields.append({"name": "Real name", "value": realname[:DISCORD_MAX_EMBED_FIELD], "inline": False})
fields.append({"name": "Email", "value": str(row.get("email") or "(none)"), "inline": False})
reg = row.get("registrationDate")
fields.append({"name": "Reg time", "value": str(reg) if reg is not None else "(unknown)", "inline": False})
return {
"title": "New account registered",
"color": EMBED_COLOR,
"fields": fields,
}
def post_embed(webhook_url, embed):
"""POST a single embed to the Discord webhook, honoring 429 rate limits."""
payload = json.dumps({"embeds": [embed]}).encode("utf-8")
for attempt in range(MAX_RATELIMIT_RETRIES):
req = urllib.request.Request(
webhook_url,
data=payload,
headers={"Content-Type": "application/json", "User-Agent": "servatrice-account-monitor/1.0"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status in (200, 204):
return True
log.warning("Unexpected Discord status %s", resp.status)
return False
except urllib.error.HTTPError as err:
if err.code == 429:
retry_after = _retry_after_seconds(err)
log.warning("Rate limited by Discord; sleeping %.2fs", retry_after)
time.sleep(retry_after)
continue
log.error("Discord webhook HTTP %s: %s", err.code, err.read().decode("utf-8", "replace")[:500])
return False
except urllib.error.URLError as err:
log.error("Discord webhook connection error: %s", err)
return False
log.error("Gave up posting after %d rate-limit retries", MAX_RATELIMIT_RETRIES)
return False
def _retry_after_seconds(err):
"""Extract the retry delay (seconds) from a Discord 429 response."""
header = err.headers.get("Retry-After")
if header:
try:
return float(header)
except ValueError:
pass
try:
body = json.loads(err.read().decode("utf-8", "replace"))
return float(body.get("retry_after", 1.0))
except (ValueError, json.JSONDecodeError):
return 1.0
def main():
parser = argparse.ArgumentParser(description="Post new Servatrice account registrations to Discord.")
parser.add_argument(
"--config", "-c",
help="Path to a servatrice-style ini; reads DB settings from its [database] "
"section (hostname/database/user/password/prefix) and the webhook from "
"[discord] new_user_activation_webhook. Defaults to the CONFIG_FILE env "
"var if set.",
)
parser.add_argument("--dry-run", action="store_true", help="Log what would be posted; do not post or write state.")
parser.add_argument("--test-webhook", action="store_true", help="Send a single test message to the webhook and exit.")
parser.add_argument("--verbose", action="store_true", help="Enable debug logging.")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
config_path = args.config or os.environ.get("CONFIG_FILE")
if args.test_webhook:
cfg = get_config(config_path, require_db=False)
ok = post_embed(
cfg["webhook_url"],
{"title": "Account monitor test", "color": EMBED_COLOR,
"description": "If you can see this, the webhook is configured correctly."},
)
sys.exit(0 if ok else 1)
cfg = get_config(config_path)
try:
conn = connect(cfg)
except pymysql.MySQLError as err:
log.error("Database connection failed: %s", err)
sys.exit(1)
try:
last_id, first_run = load_state(cfg["state_file"])
if first_run:
baseline = fetch_max_id(conn, cfg["db_prefix"])
log.info("First run: seeding high-water-mark at id=%d; posting nothing.", baseline)
if not args.dry_run:
save_state(cfg["state_file"], baseline)
return
rows = fetch_new_accounts(conn, cfg["db_prefix"], last_id)
if not rows:
log.info("No new accounts since id=%d.", last_id)
return
log.info("Found %d new account(s) since id=%d.", len(rows), last_id)
# Post oldest first. Advance the mark only past accounts we successfully
# posted; on the first failure, stop so nothing after it is posted out of
# order or skipped. The failed account (and the rest) retry next run.
for row in rows:
if args.dry_run:
log.info("[dry-run] would post: id=%s name=%s email=%s reg=%s",
row["id"], row["name"], row.get("email"), row.get("registrationDate"))
continue
if not post_embed(cfg["webhook_url"], build_embed(row)):
log.error("Failed to post account id=%s; stopping. Will retry from here next run.", row["id"])
break
last_id = row["id"]
log.info("Posted account id=%s (%s)", row["id"], row["name"])
time.sleep(POST_DELAY_SECONDS)
if not args.dry_run:
save_state(cfg["state_file"], last_id)
finally:
conn.close()
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
PyMySQL==1.2.0
@@ -0,0 +1,46 @@
#!/bin/bash
set -u
set -e
SLEEPTIME=5
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
LOGAPPENDDATE=`date +%m%d%Y`
EXPIRATION=`date +%m%d%Y -d "-3 days"`
DBNAME="servatrice"
APPNAME="servatrice"
ROOTFOLDER="./backups" #set this to the root path you want backups to be stored in
BACKUPDIR="$ROOTFOLDER/$LOGAPPENDDATE/db/$APPNAME"
TABLES=(
"cockatrice_users"
"cockatrice_decklist_files"
"cockatrice_replays"
"cockatrice_buddylist"
"cockatrice_ignorelist"
"cockatrice_bans"
"cockatrice_sessions"
"cockatrice_decklist_folders"
"cockatrice_replays_access"
"cockatrice_games"
"cockatrice_games_players"
"cockatrice_uptime"
"cockatrice_schema_version"
"cockatrice_servermessages"
"cockatrice_servers"
"cockatrice_rooms"
"cockatrice_rooms_gametypes"
)
PROCESSNAME="mysqldump"
if [ "$(pgrep $PROCESSNAME)" == "" ];
then
[ ! -d $BACKUPDIR ] && mkdir -p $BACKUPDIR/
for TABLENAME in "${TABLES[@]}"
do
BACKUPFILE="$BACKUPDIR/$APPNAME.$TABLENAME.sql.$LOGAPPENDDATE"
echo "Backing up DB Table [$TABLENAME]"
ionice -c3 nice -n19 mysqldump --defaults-file=$SQLCONFFILE $DBNAME $TABLENAME > $BACKUPFILE
sleep $SLEEPTIME
done
rm -rf "$ROOTFOLDER/$EXPIRATION/"
else
echo "Backup in progress, aborting"
fi
@@ -0,0 +1,52 @@
#!/bin/bash
set -u
set -e
SLEEPTIME=5
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
LOGAPPENDDATE=`date +%m%d%Y`
EXPIRATION=`date +%m%d%Y -d "-3 days"`
DBNAME="servatrice"
APPNAME="servatrice"
ROOTFOLDER="./backups" #set this to the root path that contains the backup files
BACKUPDIR="$ROOTFOLDER/$LOGAPPENDDATE/db/$APPNAME"
TABLES=(
"cockatrice_users"
"cockatrice_decklist_files"
"cockatrice_replays"
"cockatrice_buddylist"
"cockatrice_ignorelist"
"cockatrice_bans"
"cockatrice_sessions"
"cockatrice_decklist_folders"
"cockatrice_replays_access"
"cockatrice_games"
"cockatrice_games_players"
"cockatrice_uptime"
"cockatrice_schema_version"
"cockatrice_servermessages"
"cockatrice_servers"
"cockatrice_rooms"
"cockatrice_rooms_gametypes"
)
PROCESSNAME="mysqldump"
if [ "$(pgrep $PROCESSNAME)" == "" ];
then
[ ! -d $BACKUPDIR ] && mkdir -p $BACKUPDIR/
for TABLENAME in "${TABLES[@]}"
do
BACKUPFILE="$BACKUPDIR/$APPNAME.$TABLENAME.sql.$LOGAPPENDDATE"
if [ -f "$BACKUPFILE" ]
then
echo "Restoring up DB Table [$TABLENAME]"
ionice -c3 nice -n19 mysql --defaults-file=$SQLCONFFILE $DBNAME < $BACKUPFILE
sleep $SLEEPTIME
else
echo "Missing backup file [$$TABLENAME]"
sleep $SLEEPTIME
fi
done
rm -rf "$ROOTFOLDER/$EXPIRATION/"
else
echo "Restore in progress, aborting"
fi
@@ -0,0 +1,8 @@
#!/bin/bash
#USE THIS SCRIPT TO IDENTIFY THE SIZE OF YOUR TABLES IN THE DATABASE
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -e 'SELECT table_name AS "Tables", round(((data_length + index_length) / 1024 / 1024), 2) "Size in MB" FROM information_schema.TABLES WHERE table_schema = "'$DBNAME'" ORDER BY (data_length + index_length) DESC;'
@@ -0,0 +1,36 @@
#!/bin/bash
# THIS SCRIPT EXPECTS TO BE EXECUTED FROM THE GITHUB SOURCE FOLDER PATH STRUCTURE
# OTHERWISE, UPDATE THE 'COUNTRYCODEIMAGEPATH' TO POINT TO THE FOLDER CONTAINING THE COUNTRY CODE IMAGES
# USE THIS SCRIPT TO COMPARE EXISTING USER ACCOUNTS TO VALID COUNTRY CODES AND CLEAR INVALID COUNTRY CODE DATA
MODE="report" #set this to correct to fix invalid country codes, otherwise it only reports
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
COUNTRYCODEIMAGEPATH='../../../cockatrice/resources/countries'
VALIDCOUNT=0
INVALIDCOUNT=0
for i in `mysql --defaults-file=$SQLCONFFILE -h localhost -e "select distinct(country) from ""$DBNAME"".""$TABLEPREFIX""_users;"`
do
if [ "$i" != "country" ]; then
if [ -f "$COUNTRYCODEIMAGEPATH/$i.svg" ]; then
((VALIDCOUNT++))
else
((INVALIDCOUNT++))
if [ "$MODE" == "correct" ]; then
echo "$i COUNTRY CODE INVALID, ATTEMPTING TO CORRECT"
mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = '' where country = '$i';"
fi
fi
fi
done
if [ "$MODE" == "correct" ]; then
mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = lower(country);"
fi
echo "INVALID: $INVALIDCOUNT"
echo "VALID: $VALIDCOUNT"
@@ -0,0 +1,9 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB ON A REGULAR BASIS. NUMBER OF DAYS IS THE AMOUNT OF DAYS TO KEEP INACTIVE ACCOUNTS (EX: 1 DAY REMOVES ALL INACTIVE ACCOUNTS OLDER THAN A SINGLE DAY).
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names with in the database
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
NUMBEROFDAYS=5 #set this to the number of days to search for
mysql --defaults-file=$SQLCONFFILE -h localhost -e "delete from ""$DBNAME"".""$TABLEPREFIX""_users where active = 0 AND registrationDate < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY);"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
#SCHEDULE WITH CRONTAB AND ADJUST THE INTERVALS FOR THE NUMBER OF DAYS OF LOGS TO KEEP IN THE DATABASE
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
NUMBEROFDAYS=10 #set this to the number of days desired
mysql --defaults-file=$SQLCONFFILE -h localhost -e 'delete from ""$DBNAME"".""$TABLEPREFIX""_log where log_time < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY)'
@@ -0,0 +1,8 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB TO RUN ON A REGULAR BASIS
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set privlevel = 'NONE' where privelevel != 'NONE" AND privlevelEndDate < NOW()"
@@ -0,0 +1,7 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB DAILY
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -h localhost -e 'delete from servatrice.cockatrice_games where time_finished < DATE_SUB(now(), INTERVAL 8 DAY)'
@@ -0,0 +1,9 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB TO RUN ON A REGULAR BASIS
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
NUMBEROFDAYS=10 #set this to the number of days desired
mysql --defaults-file=$SQLCONFFILE -h localhost -e "delete from ""$DBNAME"".""$TABLEPREFIX""_sessions where start_time < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY)"
@@ -0,0 +1,8 @@
#!/bin/bash
# SCRIPT TO ADD THE FIRST ADMIN USER NAMED SERVATRICE WITH THE PASSWORD OF PASSWORD
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -h localhost -e "insert into ""$DBNAME"".""$TABLEPREFIX""_users ((admin,name,password_sha512,active,realname,email,country,avatar_bmp,registrationDate,clientID,adminnotes,privlevelStartDate,privlevelEndDate) values (1,'servatrice','jbB4kSWDmjaVzMNdU13n73SpdBCJTCJ/JYm5ZBZvfxlzbISbXir+e/aSvMz86KzOoaBfidxO0s6GVd8t00qC0TNPl+udHfECaF7MsA==',1,'servatrice','servatrice@localhost','us','null.bmp','1970-01-01 10:00:00','','','1970-01-01 10:00:00','9999-01-01 10:00:00');
+3
View File
@@ -0,0 +1,3 @@
[client]
user={db_username}
password={db_password}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python
import socket, sys, struct, time
from pypb.server_message_pb2 import ServerMessage
from pypb.session_commands_pb2 import Command_Register as Reg
from pypb.commands_pb2 import CommandContainer as Cmd
from pypb.event_server_identification_pb2 import Event_ServerIdentification as ServerId
from pypb.response_pb2 import Response
HOST = "localhost"
PORT = 4748
CMD_ID = 1
def build_reg():
global CMD_ID
cmd = Cmd()
sc = cmd.session_command.add()
reg = sc.Extensions[Reg.ext]
reg.user_name = "testUser"
reg.email = "test@example.com"
reg.password = "password"
cmd.cmd_id = CMD_ID
CMD_ID += 1
return cmd
def send(msg):
packed = struct.pack('>I', len(msg))
sock.sendall(packed)
sock.sendall(msg)
def print_resp(resp):
print "<<<"
print repr(resp)
m = ServerMessage()
m.ParseFromString(bytes(resp))
print m
def recv(sock):
print "< header"
header = sock.recv(4)
msg_size = struct.unpack('>I', header)[0]
print "< ", msg_size
raw_msg = sock.recv(msg_size)
print_resp(raw_msg)
if __name__ == "__main__":
address = (HOST, PORT)
sock = socket.socket()
print "Connecting to server ", address
sock.connect(address)
# hack for old xml clients - server expects this and discards first message
print ">>> xml hack"
xmlClientHack = Cmd().SerializeToString()
send(xmlClientHack)
print sock.recv(60)
recv(sock)
print ">>> register"
r = build_reg()
print r
msg = r.SerializeToString()
send(msg)
recv(sock)
print "Done"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env xdg-open
[Desktop Entry]
Version=1.0
Type=Application
Name=Servatrice
Exec=servatrice
Icon=servatrice
Categories=Game;CardGame;
Terminal=true
Comment=Game server for Cockatrice
+432
View File
@@ -0,0 +1,432 @@
; Servatrice configuration file
;
; This is the main configuration file for Servatrice; while using a configuration is not mandatory,
; you may want to customize some aspects of your servatrice instance, like its name, port or the way
; users can authenticate to the server.
;
; Can be passed to servatrice with --config /path/to/servatrice.ini
[server]
; This is the name that servatrice exposes to the users; the default value is pretty boring
name="My Cockatrice server"
; Multiple servatrice servers can run on the same host using the same database; each server instance
; must have a different id; the default id is 1
id=1
; The IP address servatrice will listen on for clients; defaults to "any" to listen on all the interfaces,
; a specific IPv4/v6 addresses can be used, eg for localhost-only 127.0.0.1 for IPv4 or ::1 for IPv6.
host=any
; The TCP port number Servatrice will listen on for clients; default is 4747;
; Will be removed in the future, use websocket connection instead
port=4747
; Servatrice can scale up to serve big number of users using more than one parallel thread of execution;
; If your server is hosting a lot of players and they frequently report of being unable to login or
; long delays (lag), you may want to try increasing this value; default is 1.
; Set to 0 to disable the tcp server.
number_pools=1
; Servatrice can listen for clients on websockets, too. Multiple connection pools are available but
; unfortunately, due to a Qt limitation, they must run in the same execution thread.
; Set to 0 to disable the websocket server.
websocket_number_pools=1
; The IP address servatrice will listen on for websockets clients; defaults to "any"
websocket_host=any
; The TCP port number servatrice will listen on for websockets clients; default is 4748
websocket_port=4748
; When database is enabled, servatrice writes the server status in the "update" database table; this
; setting defines every how many milliseconds servatrice will update its status; default is 15000 (15 secs)
statusupdate=15000
; Do you want servatrice to write important events and errors to a logfile? Default is 1 (yes).
writelog=1
; Choose a name for the log file, if enabled; you can specify an absolute path or a path relative to
; the servatrice executable; the default file name is server.log (in the same path as servatrice)
; Note: When running servatrice under windows you will need to use double backslashes between folder locations
; [ex: C:\\Temp\\server.log ]
logfile=server.log
; You may want to log only certain messages in the logfile. The default log level is extremely verbose.
; This setting should contain a comma-separated list of strings that will be selectively logged.
; All other lines will be excluded from the log. Default is empty; example: "Registration,_Login,foobar"
logfilters=""
; Set the time interval in seconds that servatrice will use to communicate with each connected client
; to verify the client has not timed out. Defaults is 1 seconds
clientkeepalive=1
; Maximum time in seconds a player can stay inactive with there client not even responding to pings, before is
; considered disconnected; default is 15
max_player_inactivity_time=15
; More modern clients generate client IDs based on specific client side information. Enable this option to
; require that clients report the client ID in order to log into the server. Default is false
requireclientid=false
; You can limit the types of clients that connect to the server by requiring different features be available
; on the client. This setting can contain a comma-seperated list of features. if any of the features
; listed in this line are not available on the client the client will be denied access to the server upon
; attempting to log in. Example: "client_id,client_ver,websocket"
requiredfeatures=""
; You can define custom warnings that users are sent when the moderation staff uses the right client warn user
; menu option. This list is comma seperated that each item will appear in the drop down list for staff members
; to choose from. Example: "Flaming,Foul Language"
officialwarnings="Flaming,Spamming,Causing Drama,Abusive Language"
; Maximum time in seconds a player can stay connected but idle. Default is 3600 (0 = disabled)
; Clients will be notified at the 90% time period of pending disconnection if they do not take action.
idleclienttimeout=3600
[authentication]
; Servatrice can authenticate users connecting. It currently supports 3 different authentication methods:
; * none: no authentication, accept every user;
; * password: require users to specify a common password to log in;
; * sql: authenticate users against the "users" table of the database;
; Please note that only the "sql" method permits to have registered users and store their data on the server.
method=none
; if the chosen authentication method is password, here you can define the password your users will use to log in
password=123456
; Accept only registered users? default is false (accept unregistered users)
regonly=false
[users]
; The minimum length a username can be
minnamelength=6
; The maximum length a username can be
maxnamelength=12
; If a username should be allowed to contain lowercase chars [a-z]
allowlowercase=true
; If a username should be allowed to conatain uppercase chars [A-Z]
allowuppercase=true
; If a username should be allowed to contain numbers [0-9]
allownumerics=true
; Define punctuation allowed in usernames
allowedpunctuation=_.-
; If a username can begin with punctuation defined in allowedpunctuation
allowpunctuationprefix=false
; Disallow usernames containing these words. This list is comma seperated, e.g.
; "admin,user,name"
disallowedwords="admin"
; Overwrite the words shown to the user when they enter a wrong username,
; use \n to start a new line. Neither the real wordlist nor the disallowed
; expressions will be sent to the user if this is set.
; In the old versions of the client this list will be prefaced with
; "can not contain any of the following words:"
; example:
;displaydisallowedwords="no attempts at impersonating staff\nno unparliamentary language\nno references to controversial figures\nstaff reserves the right to remove accounts deemed inappropriate"
; Setting it to nothing will simply hide the list:
;displaydisallowedwords=
; Disallow usernames matching these regular expressions. This list is comma
; separated, e.g. "\\w+\\d+,\\d{2}user", hence you cannot use commas in your
; expressions. Backslashes must be escaped, so `\w+\d+` becomes `\\w+\\d+`.
; WARNING: Complex expressions can be harmful to performance. Please make sure
; your expressions are considered well formed. See this page for info:
; http://www.regular-expressions.info/catastrophic.html
disallowedregexp=""
; Define minimum password length
; Default 6.
minpasswordlength = 6
[registration]
; Servatrice can process registration requests to add new users on the fly.
; Enable this feature? Default false.
;enabled=false
; Require users to provide an email address in order to register. Default true.
;requireemail=true
; Require email activation. Newly registered users will receive an activation token by email,
; and will be required to input back this token on cockatrice at the first login to get their
; account activated. Default true.
;requireemailactivation=true
; Set this number to the maximum number of accounts any one user can use to create new accounts
; using the same email address. 0 = Unlimited number of accounts (default).
;maxaccountsperemail=0
; You can prevent users from using certain mail domains for registration. This setting contains a
; comma-seperated list of email provider domains that you would like to prevent users from using
; during registration. Comparison's are implicit, so placing an entry such as mail.com will also
; prevent users from registering accounts with providers such as gmail.com and hotmail.com
; Example: "10minutemail.com,gmail.com"
;emailproviderblacklist=""
; You can require users to only use certain email domains for registration. This setting is a
; comma-separated list of email provider domains that you have explicitly audited and require
; the use of in order to create an account. Comparison's are explicit, so you must specify the
; domain in completion, such as gmail.com and hotmail.com. Email whitelist is checked before
; Email blacklist is checked, so an email cannot be in both setting configurations.
; Example: "gmail.com,hotmail.com,icloud.com"
;emailproviderwhitelist=""
[forgotpassword]
; Servatrice can process reset password requests allowing users to reset their account
; passwords in the event they forget it. Should this feature be enabled? Default: false.
; enable=false
; Reset password request should not be allowed to stay valid forever. This settings
; informs servatrice how long a players reset password reset token is valid for (in minutes).
; Default: 60
; tokenlife=60
; Servatrice can challenge users that are making reset password requests to answer
; questions in regards to their account to help validate they are the true owner of the account.
; Should this feature be enabled? Default: false
; enablechallenge=false
; Email subject for the reset password emails
; subject="Cockatrice reset password token"
; Reset password email body. You can use these tags here: %username %token
; They will be substituted with the actual values in the email
;
; body="Hi %username,\r\nthanks for reaching out to us with your password reset request for our Cockatrice server.\r\nHere's your unique token in order to reset your account password in the app:\r\n\r\n%token\r\n\r\nHappy gaming!"
[smtp]
; Enable the internal smtp client to send registration emails. If you would like to
; use some other method to send email activation tokens set this value to false. Otherwise
; setting it to true (default) the server will send canned generated emails containing
; activation tokens for you during update intervals. Setting this to false will require
; you to either manually activate user accounts or manually send users the activation token
; by whatever means.
enableinternalsmtpclient=true
; Connectin type: currently supported method are "tcp" and "ssl"; tls is autodetected if available
connection=tcp
; Accept all certificates: in ssl mode, enable this if your server is using an invalid/self signed certificate
acceptallcerts=false;
; Hostname or IP addres of the smtp server
host=localhost
; Smtp port number of the smtp server. Usual values are 25 or 587 for tcp, 465 for ssl
port=25
; Username: this typically matches the "from" email address
username=root@localhost
; Password for the username
password=foobar
; Sender email address: the "from" email address
email=root@localhost
; Sender email name
name="Cockatrice server"
; Email subject
subject="Cockatrice server account activation token"
; Email body. You can use these tags here: %username %token
; They will be substituted with the actual values in the email
;
body="Hi %username, thank our for registering on our Cockatrice server\r\nHere's the activation token you need to supply for activating your account:\r\n\r\n%token\r\n\r\nHappy gaming!"
[database]
; Database type. Valid values are:
; * none: no database;
; * mysql: mysql or compatible database;
type=none
; Prefix used in he database for table names; default is cockatrice
prefix=cockatrice
; Database connection parameter: server hostname or IP
hostname=localhost
; Database connection parameter: database name
database=servatrice
; Database connection parameter: database user
user=servatrice
; Database connection parameter: database user's password
password=foobar
[rooms]
; A servatrice server can expose to the users different "rooms" to chat and create games. Rooms can be defined
; with two different methods:
; config: rooms are defined in this configuration (see the following example)
; sql: rooms are defined in the "rooms" table of the database
method=config
; Example configuration for a server with rooms configured in the configuration file. Number of rooms defined
roomlist\size=1
; Room name for the room number 1
roomlist\1\name="General room"
; Room description for the room number 1
roomlist\1\description="Play anything here."
; Rooms can restrict the level of user that can join. Current supported options are none, registered, moderator, administrator.
; Default is none.
roomlist\1\permissionlevel=none
; Rooms can restrict the permission level of users that can join, Currnetly supported options are none, privileged, vip, and donator.
; Default is none.
roomlist\1\privilegelevel=none
; Wether to make users autojoin this room when connected to the server
roomlist\1\autojoin=true
; Message displayed to each user when he joins room number 1
roomlist\1\joinmessage="This message is only here to show that rooms can have a join message."
; The number of chat history messages to save that gets presented to a user joining the room
roomlist\1\chathistorysize=100
; Number of game types allowed (defined) in the room number 1
roomlist\1\game_types\size=3
; Name of the three game types for the room number 1
roomlist\1\game_types\1\name="GameType1"
roomlist\1\game_types\2\name="GameType2"
roomlist\1\game_types\3\name="GameType3"
[game]
; Maximum time in seconds all players in a game can stay inactive before the game is automatically closed;
; default is 120
max_game_inactivity_time=120
; All actions during a game are recorded and stored in the database as a replay that all participants of
; the game can go back to and review after the game is closed. This can require a fairly large amount of
; storage to save all the information. Disable this option to prevent the storing of replay data in
; the database. Default value is true.
store_replays=true
; Allow users to create a new game and join it as a judge. The host will be able to execute any action on
; the cards of every player. This is needed in order to support some games (eg. Werewolf).
; Default off to prevent abuse on servers that are mostly running other games.
allow_create_as_judge=false
[security]
; You may want to restrict the number of users that can connect to your server at any given time.
enable_max_user_limit=false
; Maximum number of users that can connect to the server, default is 500.
max_users_total=500
; Maximum number of users that can connect to the server using a tcp connection, default is 500.
max_users_tcp=500
; Maximum number of users that can connect to the server using a websocket connection, default is 500.
max_users_websocket=500
; Maximum number of users that can connect from the same IP address; useful to avoid bots, default is 4
max_users_per_address=4
; You may want to allow an unlimited number of users from a trusted source. This setting can contain a
; comma-separed list of IP addresses which will allow an unlimited number of connections from each of the
; IP addresses listed (ignoring the max_users_per_address). Default is "127.0.0.1,::1"; example: "192.73.233.244,81.4.100.74"
trusted_sources="127.0.0.1,::1"
; Servatrice can avoid users from flooding rooms with large number of messages in an interval of time.
; This setting defines the length in seconds of the considered interval; default is 10
message_counting_interval=10
; Maximum size in characters of all messages in an interval before new messages gets dropped; default is 1000
max_message_size_per_interval=1000
; Maximum number of messages in an interval before new messages gets dropped; default is 10
max_message_count_per_interval=10
; Maximum number of games a single user can create; default is 5; set to -1 to disable; 0 disallows game creation
max_games_per_user=5
; Servatrice can avoid users from flooding games with large number of game commands in an interval of time.
; This setting defines the length in seconds of the considered interval; default is 10
command_counting_interval=10
; Maximum number of game commands in an interval before new commands gets dropped; default is 20
max_command_count_per_interval=20
[logging]
; Admin/Moderators can query the stored logs for information when looking up reports by various players. This
; option can allow or disallow them from doing so.
; !!NOTE!! Enabling this feature puts a very high CPU and DISK load on the server, enable with caution.
enablelogquery=false
; Servatrice can log user messages to the database table cockatrice_log.
; These messages can come from different sources; each source can be enabled separately.
; Log user messages inside chat rooms
log_user_msg_room=false
; Log user messages inside games
log_user_msg_game=false
; Log user messages in private chats
log_user_msg_chat=false
; Log user messages coming from other servers in the network
log_user_msg_isl=false
[audit]
; Servatrice can record certain actions being performed in the database for server operators to better understand
; if some one may be abusing application functionality. Enabling auditing will allow servatrice to record any
; of the below enabled audit functionality to be recorded.
; Default: true
enable_audit=true
; Servatrice can record when users attempt a new account registration. Should we enable auditing for this action?
; Default: true
enable_registration_audit=true
; Servatrice can record when a users attempts to reset the account password. Should we enable auditing for this action?
; Default: true
enable_forgotpassword_audit=true
; EXPERIMENTAL - NOT WORKING YET
; The following settings are relative to the server network functionality, that is not yet complete.
; Avoid enabling it unless you are willing to test it and help its development.
[servernetwork]
; Servatrice servers can connect themselves and build a network. This settins enable the ability of servatrice
; of waiting for other server's connections and connect to other servers. Other servers can be defined in the
; "servers" table of the database. Default is 0 (disabled)
active=0
; The TCP port number Servatrice will listen on for other servers; default is 14747
port=14747
; Server-to-server communication needs a valid certificate in PEM format. Enter its filename in this setting
ssl_cert=ssl_cert.pem
; Filename of the private key for the server-to-server certificate
ssl_key=ssl_key.pem
+5
View File
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/" >
<file alias="resources/appicon.svg">resources/servatrice.svg</file>
</qresource>
</RCC>
+1
View File
@@ -0,0 +1 @@
ID1_ICON1 ICON DISCARDABLE "resources/appicon.ico"
+319
View File
@@ -0,0 +1,319 @@
-- Schema file for servatrice database.
-- This schema file is using the default table prefix "cockatrice",
-- to match the "prefix=cockatrice" default setting in servatrice.ini
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
-- Every time the database schema changes, the schema version number
-- must be incremented. Also remember to update the corresponding
-- number in servatrice/src/servatrice_database_interface.h
CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` (
`version` int(7) unsigned NOT NULL,
PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
INSERT INTO cockatrice_schema_version VALUES(35);
-- users and user data tables
CREATE TABLE IF NOT EXISTS `cockatrice_users` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`admin` tinyint(1) NOT NULL,
`name` varchar(35) NOT NULL,
`realname` varchar(255) NOT NULL,
`password_sha512` char(120) NOT NULL,
`email` varchar(255) NOT NULL,
`country` char(2) NOT NULL,
`avatar_bmp` mediumblob NOT NULL,
`registrationDate` datetime NOT NULL,
`active` tinyint(1) NOT NULL,
`token` binary(16),
`clientid` varchar(15) NOT NULL,
`adminnotes` mediumtext NOT NULL,
`privlevel` enum("NONE","VIP","DONATOR") NOT NULL,
`privlevelStartDate` datetime NOT NULL,
`privlevelEndDate` datetime NOT NULL,
`passwordLastChangedDate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`leftPawnColorOverride` varchar(255),
`rightPawnColorOverride` varchar(255),
`card_art_params` TEXT DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
KEY `token` (`token`),
KEY `email` (`email`),
INDEX `idx_admin` (`admin`),
INDEX `idx_active` (`active`),
INDEX `idx_privlevel` (`privlevel`),
INDEX `idx_clientid` (`clientid`),
INDEX `idx_pawnColorOverrides` (`leftPawnColorOverride`, `rightPawnColorOverride`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`id_folder` int(7) unsigned zerofill NOT NULL,
`id_user` int(7) unsigned NULL,
`name` varchar(50) NOT NULL,
`upload_time` datetime NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`id`),
KEY `FolderPlusUser` (`id_folder`,`id_user`),
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`id_parent` int(7) unsigned zerofill NOT NULL,
`id_user` int(7) unsigned NULL,
`name` varchar(30) NOT NULL,
PRIMARY KEY (`id`),
KEY `ParentPlusUser` (`id_parent`,`id_user`),
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_ignorelist` (
`id_user1` int(7) unsigned NOT NULL,
`id_user2` int(7) unsigned NOT NULL,
UNIQUE KEY `key` (`id_user1`, `id_user2`),
FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_buddylist` (
`id_user1` int(7) unsigned NOT NULL,
`id_user2` int(7) unsigned NOT NULL,
UNIQUE KEY `key` (`id_user1`, `id_user2`),
FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
-- rooms
CREATE TABLE IF NOT EXISTS `cockatrice_rooms` (
`id` int(7) unsigned NOT NULL auto_increment,
`name` varchar(50) NOT NULL,
`descr` varchar(255) NOT NULL,
`permissionlevel` enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR') NOT NULL,
`privlevel` enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL,
`auto_join` tinyint(1) default 0,
`join_message` varchar(255) NOT NULL,
`chat_history_size` int(4) NOT NULL,
`id_server` tinyint(3) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_rooms_gametypes` (
`id_room` int(7) unsigned NOT NULL,
`name` varchar(50) NOT NULL,
`id_server` tinyint(3) NOT NULL DEFAULT 1,
FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
-- games
CREATE TABLE IF NOT EXISTS `cockatrice_games` (
`room_name` varchar(255) NOT NULL,
`id` int(7) unsigned NOT NULL auto_increment,
`descr` varchar(50) default NULL,
`creator_name` varchar(35) NOT NULL,
`password` tinyint(1) NOT NULL,
`game_types` varchar(255) NOT NULL,
`player_count` tinyint(3) NOT NULL,
`time_started` datetime default NULL,
`time_finished` datetime default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_games_players` (
`id_game` int(7) unsigned zerofill NOT NULL,
`player_name` varchar(35) NOT NULL,
FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
-- Note: an empty row with id_game = NULL is created when the game is created,
-- and then updated when the game ends with the full replay data.
CREATE TABLE IF NOT EXISTS `cockatrice_replays` (
`id` int(7) NOT NULL AUTO_INCREMENT,
`id_game` int(7) unsigned NULL,
`duration` int(7) NOT NULL,
`replay` mediumblob NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_replays_access` (
`id_game` int(7) unsigned NOT NULL,
`id_player` int(7) unsigned NOT NULL,
`replay_name` varchar(255) NOT NULL,
`do_not_hide` tinyint(1) NOT NULL,
KEY `id_player` (`id_player`),
FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(`id_player`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
-- server administration
-- Note: unused table
CREATE TABLE IF NOT EXISTS `cockatrice_servers` (
`id` mediumint(8) unsigned NOT NULL,
`ssl_cert` text NOT NULL,
`hostname` varchar(255) NOT NULL,
`address` varchar(255) NOT NULL,
`game_port` mediumint(8) unsigned NOT NULL,
`control_port` mediumint(9) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_uptime` (
`id_server` tinyint(3) NOT NULL,
`timest` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`uptime` int(11) NOT NULL,
`users_count` int(11) NOT NULL,
`mods_count` int(11) NOT NULL DEFAULT 0,
`mods_list` TEXT,
`games_count` int(11) NOT NULL,
`rx_bytes` int(11) NOT NULL,
`tx_bytes` int(11) NOT NULL,
PRIMARY KEY (`timest`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_servermessages` (
`id_server` tinyint(3) not null default 1,
`timest` datetime NOT NULL default '0000-00-00 00:00:00',
`message` text,
PRIMARY KEY (`timest`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_sessions` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`user_name` varchar(35) NOT NULL,
`id_server` tinyint(3) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime DEFAULT NULL,
`clientid` varchar(15) NOT NULL,
`connection_type` ENUM('tcp', 'websocket'),
PRIMARY KEY (`id`),
KEY `username` (`user_name`),
INDEX `idx_start_time` (`start_time`),
INDEX `idx_clientid` (`clientid`),
INDEX `idx_ip_address` (`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
-- server moderation
CREATE TABLE IF NOT EXISTS `cockatrice_bans` (
`user_name` varchar(35) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`id_admin` int(7) unsigned zerofill NOT NULL,
`time_from` datetime NOT NULL,
`minutes` int(6) NOT NULL,
`reason` text NOT NULL,
`visible_reason` text NOT NULL,
`clientid` varchar(15) NOT NULL,
PRIMARY KEY (`user_name`,`time_from`),
KEY `time_from` (`time_from`,`ip_address`),
KEY `ip_address` (`ip_address`),
INDEX `idx_user_name` (`user_name`),
FOREIGN KEY(`id_admin`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_warnings` (
`user_id` int(7) unsigned NOT NULL,
`user_name` varchar(35) NOT NULL,
`mod_name` varchar(35) NOT NULL,
`reason` text NOT NULL,
`time_of` datetime NOT NULL,
`clientid` varchar(15) NOT NULL,
PRIMARY KEY (`user_id`,`time_of`),
INDEX `idx_time_of` (`time_of`),
INDEX `idx_user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_log` (
`log_time` datetime NOT NULL,
`sender_id` int(7) unsigned NULL,
`sender_name` varchar(35) NOT NULL,
`sender_ip` varchar(45) NOT NULL,
`log_message` text NOT NULL,
`target_type` ENUM('room', 'game', 'chat'),
`target_id` int(7) NULL,
`target_name` varchar(50) NOT NULL,
KEY `sender_name` (`sender_name`),
KEY `sender_ip` (`sender_ip`),
KEY `target_id` (`target_id`),
KEY `target_name` (`target_name`),
INDEX `idx_log_time` (`log_time`),
FOREIGN KEY(`sender_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
-- No FK on target_id, it can be zero
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_activation_emails` (
`name` varchar(35) NOT NULL,
FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_user_analytics` (
`id` int(7) unsigned zerofill NOT NULL,
`client_ver` varchar(35) NOT NULL,
`last_login` datetime NOT NULL,
`notes` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_last_login` (`last_login`),
FOREIGN KEY(`id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_donations` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(35) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`payment_pre_fee` double DEFAULT NULL,
`payment_post_fee` double DEFAULT NULL,
`term_length` int(11) DEFAULT NULL,
`date` varchar(255) DEFAULT NULL,
`pp_type` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_forgot_password` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`name` varchar(35) NOT NULL,
`requestDate` datetime NOT NULL default '0000-00-00 00:00:00',
`emailed` tinyint(1) NOT NULL default 0,
PRIMARY KEY (`id`),
KEY `user_name` (`name`),
INDEX `idx_emailed` (`emailed`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_audit` (
`id` int(7) unsigned zerofill NOT NULL auto_increment,
`id_server` tinyint(3) NOT NULL,
`name` varchar(35) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`clientid` varchar(15) NOT NULL,
`incidentDate` datetime NOT NULL default '0000-00-00 00:00:00',
`action` varchar(35) NOT NULL,
`results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail',
`details` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`card_name` varchar(255) NOT NULL,
`card_provider_id` varchar(255) NOT NULL,
`mode` enum('ALLOW','DENY') NOT NULL,
`reason` varchar(255) DEFAULT NULL,
`created_by` int(7) unsigned DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`),
KEY `idx_mode` (`mode`),
FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
+61
View File
@@ -0,0 +1,61 @@
#include "email_parser.h"
#include <QRegularExpression>
#include <QString>
QPair<QString, QString> EmailParser::parseEmailAddress(const QString &dirtyEmailAddress)
{
// https://www.regular-expressions.info/email.html
static const QRegularExpression emailRegex(R"(^([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,})$)",
QRegularExpression::CaseInsensitiveOption);
const auto match = emailRegex.match(dirtyEmailAddress);
if (dirtyEmailAddress.isEmpty() || !match.hasMatch()) {
return {};
}
QString capturedEmailUser = match.captured(1);
QString capturedEmailAddressDomain = match.captured(2);
// Replace googlemail.com with gmail.com, as is standard nowadays
// https://www.gmass.co/blog/domains-gmail-com-googlemail-com-and-google-com/
if (capturedEmailAddressDomain.toLower() == "googlemail.com") {
capturedEmailAddressDomain = "gmail.com";
}
// Trim out dots and pluses from Google/Gmail domains
if (capturedEmailAddressDomain.toLower() == "gmail.com") {
// Remove all content after the first plus sign (as unnecessary with gmail)
// https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html
const auto firstPlusSign = capturedEmailUser.indexOf("+");
if (firstPlusSign != -1) {
capturedEmailUser = capturedEmailUser.left(firstPlusSign);
}
// Remove all periods (as unnecessary with gmail)
// https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html
capturedEmailUser.replace(".", "");
}
// Trim out minuses from Yahoo domains
else if (capturedEmailAddressDomain.toLower() == "yahoo.com") {
const auto firstMinusSign = capturedEmailUser.indexOf("-");
if (firstMinusSign != -1) {
capturedEmailUser = capturedEmailUser.left(firstMinusSign);
}
}
return {capturedEmailUser, capturedEmailAddressDomain};
}
QString EmailParser::getParsedEmailAddress(const QString &dirtyEmailAddress)
{
const auto parsedEmailAddress = EmailParser::parseEmailAddress(dirtyEmailAddress);
return EmailParser::getParsedEmailAddress(parsedEmailAddress);
}
QString EmailParser::getParsedEmailAddress(const QPair<QString, QString> &emailAddressIntermediate)
{
const auto emailUser = emailAddressIntermediate.first;
const auto emailDomain = emailAddressIntermediate.second;
return emailUser + "@" + emailDomain;
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef COCKATRICE_EMAILPARSER_H
#define COCKATRICE_EMAILPARSER_H
#include <QPair>
#include <QString>
class EmailParser
{
public:
static QPair<QString, QString> parseEmailAddress(const QString &dirtyEmailAddress);
static QString getParsedEmailAddress(const QString &dirtyEmailAddress);
static QString getParsedEmailAddress(const QPair<QString, QString> &emailAddressIntermediate);
};
#endif // COCKATRICE_EMAILPARSER_H
+482
View File
@@ -0,0 +1,482 @@
#include "isl_interface.h"
#include "main.h"
#include "server_logger.h"
#include <QLoggingCategory>
#include <QSslSocket>
#include <google/protobuf/descriptor.h>
#include <libcockatrice/protocol/debug_pb_message.h>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
#include <libcockatrice/protocol/pb/event_join_room.pb.h>
#include <libcockatrice/protocol/pb/event_leave_room.pb.h>
#include <libcockatrice/protocol/pb/event_list_games.pb.h>
#include <libcockatrice/protocol/pb/event_remove_messages.pb.h>
#include <libcockatrice/protocol/pb/event_room_say.pb.h>
#include <libcockatrice/protocol/pb/event_server_complete_list.pb.h>
#include <libcockatrice/protocol/pb/event_user_joined.pb.h>
#include <libcockatrice/protocol/pb/event_user_left.pb.h>
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
#include <libcockatrice/protocol/pb/isl_message.pb.h>
#include <server_protocolhandler.h>
#include <server_room.h>
inline Q_LOGGING_CATEGORY(IslInterfaceLog, "isl_interface");
void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey)
{
socket = new QSslSocket(this);
socket->setLocalCertificate(cert);
socket->setPrivateKey(privateKey);
connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection);
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this,
SLOT(catchSocketError(QAbstractSocket::SocketError)));
connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection);
}
IslInterface::IslInterface(int _socketDescriptor,
const QSslCertificate &cert,
const QSslKey &privateKey,
Servatrice *_server)
: QObject(), socketDescriptor(_socketDescriptor), server(_server), messageInProgress(false)
{
sharedCtor(cert, privateKey);
}
IslInterface::IslInterface(int _serverId,
const QString &_peerHostName,
const QString &_peerAddress,
int _peerPort,
const QSslCertificate &_peerCert,
const QSslCertificate &cert,
const QSslKey &privateKey,
Servatrice *_server)
: QObject(), serverId(_serverId), peerHostName(_peerHostName), peerAddress(_peerAddress), peerPort(_peerPort),
peerCert(_peerCert), server(_server), messageInProgress(false)
{
sharedCtor(cert, privateKey);
}
IslInterface::~IslInterface()
{
logger->logMessage("[ISL] session ended", this);
flushOutputBuffer();
// As these signals are connected with Qt::QueuedConnection implicitly,
// we don't need to worry about them modifying the lists while we're iterating.
server->roomsLock.lockForRead();
QMapIterator<int, Server_Room *> roomIterator(server->getRooms());
while (roomIterator.hasNext()) {
Server_Room *room = roomIterator.next().value();
room->usersLock.lockForRead();
QMapIterator<QString, ServerInfo_User_Container> roomUsers(room->getExternalUsers());
while (roomUsers.hasNext()) {
roomUsers.next();
if (roomUsers.value().getUserInfo()->server_id() == serverId) {
emit externalRoomUserLeft(room->getId(), roomUsers.key());
}
}
room->usersLock.unlock();
}
server->roomsLock.unlock();
server->clientsLock.lockForRead();
QMapIterator<QString, Server_AbstractUserInterface *> extUsers(server->getExternalUsers());
while (extUsers.hasNext()) {
extUsers.next();
if (extUsers.value()->getUserInfo()->server_id() == serverId) {
emit externalUserLeft(extUsers.key());
}
}
server->clientsLock.unlock();
}
void IslInterface::initServer()
{
socket->setSocketDescriptor(socketDescriptor);
logger->logMessage(QString("[ISL] incoming connection: %1").arg(socket->peerAddress().toString()));
QList<ServerProperties> serverList = server->getServerList();
int listIndex = -1;
for (int i = 0; i < serverList.size(); ++i) {
if (serverList[i].address == socket->peerAddress()) {
listIndex = i;
break;
}
}
if (listIndex == -1) {
logger->logMessage(
QString("[ISL] address %1 unknown, terminating connection").arg(socket->peerAddress().toString()));
deleteLater();
return;
}
socket->startServerEncryption();
if (!socket->waitForEncrypted(5000)) {
QList<QSslError> sslErrors(socket->sslHandshakeErrors());
if (sslErrors.isEmpty()) {
qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection";
} else {
qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors;
}
deleteLater();
return;
}
if (serverList[listIndex].cert == socket->peerCertificate()) {
logger->logMessage(QString("[ISL] Peer authenticated as " + serverList[listIndex].hostname));
} else {
logger->logMessage(QString("[ISL] Authentication failed, terminating connection"));
deleteLater();
return;
}
serverId = serverList[listIndex].id;
Event_ServerCompleteList event;
event.set_server_id(server->getServerID());
server->clientsLock.lockForRead();
QMapIterator<QString, Server_ProtocolHandler *> userIterator(server->getUsers());
while (userIterator.hasNext()) {
event.add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(true, true));
}
server->clientsLock.unlock();
server->roomsLock.lockForRead();
QMapIterator<int, Server_Room *> roomIterator(server->getRooms());
while (roomIterator.hasNext()) {
Server_Room *room = roomIterator.next().value();
room->usersLock.lockForRead();
room->gamesLock.lockForRead();
room->getInfo(*event.add_room_list(), true, true, false);
}
IslMessage message;
message.set_message_type(IslMessage::SESSION_EVENT);
SessionEvent *sessionEvent = message.mutable_session_event();
sessionEvent->GetReflection()
->MutableMessage(sessionEvent, event.GetDescriptor()->FindExtensionByName("ext"))
->CopyFrom(event);
server->islLock.lockForWrite();
if (server->islConnectionExists(serverId)) {
qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection";
deleteLater();
} else {
transmitMessage(message);
server->addIslInterface(serverId, this);
}
server->islLock.unlock();
roomIterator.toFront();
while (roomIterator.hasNext()) {
roomIterator.next();
roomIterator.value()->gamesLock.unlock();
roomIterator.value()->usersLock.unlock();
}
server->roomsLock.unlock();
}
void IslInterface::initClient()
{
QList<QSslError> expectedErrors;
expectedErrors.append(QSslError(QSslError::SelfSignedCertificate, peerCert));
socket->ignoreSslErrors(expectedErrors);
qCDebug(IslInterfaceLog) << "Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort;
socket->connectToHostEncrypted(peerAddress, peerPort, peerHostName);
if (!socket->waitForConnected(5000)) {
qCDebug(IslInterfaceLog) << "Socket error:" << socket->errorString();
deleteLater();
return;
}
if (!socket->waitForEncrypted(5000)) {
QList<QSslError> sslErrors(socket->sslHandshakeErrors());
if (sslErrors.isEmpty()) {
qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection";
} else {
qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors;
}
deleteLater();
return;
}
server->islLock.lockForWrite();
if (server->islConnectionExists(serverId)) {
qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection";
deleteLater();
return;
}
server->addIslInterface(serverId, this);
server->islLock.unlock();
}
void IslInterface::flushOutputBuffer()
{
QMutexLocker locker(&outputBufferMutex);
if (outputBuffer.isEmpty()) {
return;
}
server->incTxBytes(outputBuffer.size());
socket->write(outputBuffer);
socket->flush();
outputBuffer.clear();
}
void IslInterface::readClient()
{
QByteArray data = socket->readAll();
server->incRxBytes(data.size());
inputBuffer.append(data);
do {
if (!messageInProgress) {
if (inputBuffer.size() >= 4) {
messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) +
(((quint32)(unsigned char)inputBuffer[1]) << 16) +
(((quint32)(unsigned char)inputBuffer[2]) << 8) +
((quint32)(unsigned char)inputBuffer[3]);
inputBuffer.remove(0, 4);
messageInProgress = true;
} else {
return;
}
}
if (inputBuffer.size() < messageLength) {
return;
}
IslMessage newMessage;
bool ok = newMessage.ParseFromArray(inputBuffer.data(), messageLength);
inputBuffer.remove(0, messageLength);
messageInProgress = false;
if (ok) {
processMessage(newMessage);
} else {
qCWarning(IslInterfaceLog) << "parsing error!";
}
} while (!inputBuffer.isEmpty());
}
void IslInterface::catchSocketError(QAbstractSocket::SocketError socketError)
{
qCWarning(IslInterfaceLog) << "Socket error:" << socketError;
server->islLock.lockForWrite();
server->removeIslInterface(serverId);
server->islLock.unlock();
deleteLater();
}
void IslInterface::transmitMessage(const IslMessage &item)
{
QByteArray buf;
#if GOOGLE_PROTOBUF_VERSION > 3001000
unsigned int size = static_cast<unsigned int>(item.ByteSizeLong());
#else
unsigned int size = static_cast<unsigned int>(item.ByteSize());
#endif
buf.resize(size + 4);
if (!item.SerializeToArray(buf.data() + 4, size)) {
qCWarning(IslInterfaceLog) << "transmit error!";
return;
}
buf.data()[3] = (unsigned char)size;
buf.data()[2] = (unsigned char)(size >> 8);
buf.data()[1] = (unsigned char)(size >> 16);
buf.data()[0] = (unsigned char)(size >> 24);
outputBufferMutex.lock();
outputBuffer.append(buf);
outputBufferMutex.unlock();
emit outputBufferChanged();
}
void IslInterface::sessionEvent_ServerCompleteList(const Event_ServerCompleteList &event)
{
for (int i = 0; i < event.user_list_size(); ++i) {
ServerInfo_User temp(event.user_list(i));
temp.set_server_id(serverId);
emit externalUserJoined(temp);
}
for (int i = 0; i < event.room_list_size(); ++i) {
const ServerInfo_Room &room = event.room_list(i);
for (int j = 0; j < room.user_list_size(); ++j) {
ServerInfo_User userInfo(room.user_list(j));
userInfo.set_server_id(serverId);
emit externalRoomUserJoined(room.room_id(), userInfo);
}
for (int j = 0; j < room.game_list_size(); ++j) {
ServerInfo_Game gameInfo(room.game_list(j));
gameInfo.set_server_id(serverId);
emit externalRoomGameListChanged(room.room_id(), gameInfo);
}
}
}
void IslInterface::sessionEvent_UserJoined(const Event_UserJoined &event)
{
ServerInfo_User userInfo(event.user_info());
userInfo.set_server_id(serverId);
emit externalUserJoined(userInfo);
}
void IslInterface::sessionEvent_UserLeft(const Event_UserLeft &event)
{
emit externalUserLeft(QString::fromStdString(event.name()));
}
void IslInterface::roomEvent_UserJoined(int roomId, const Event_JoinRoom &event)
{
ServerInfo_User userInfo(event.user_info());
userInfo.set_server_id(serverId);
emit externalRoomUserJoined(roomId, userInfo);
}
void IslInterface::roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event)
{
emit externalRoomUserLeft(roomId, QString::fromStdString(event.name()));
}
void IslInterface::roomEvent_Say(int roomId, const Event_RoomSay &event)
{
emit externalRoomSay(roomId, QString::fromStdString(event.name()), QString::fromStdString(event.message()));
}
void IslInterface::roomEvent_ListGames(int roomId, const Event_ListGames &event)
{
for (int i = 0; i < event.game_list_size(); ++i) {
ServerInfo_Game gameInfo(event.game_list(i));
gameInfo.set_server_id(serverId);
emit externalRoomGameListChanged(roomId, gameInfo);
}
}
void IslInterface::roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event)
{
emit externalRoomRemoveMessages(roomId, QString::fromStdString(event.name()), event.amount());
}
void IslInterface::roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId)
{
emit joinGameCommandReceived(cmd, cmdId, roomId, serverId, sessionId);
}
void IslInterface::processSessionEvent(const SessionEvent &event, qint64 sessionId)
{
switch (getPbExtension(event)) {
case SessionEvent::SERVER_COMPLETE_LIST:
sessionEvent_ServerCompleteList(event.GetExtension(Event_ServerCompleteList::ext));
break;
case SessionEvent::USER_JOINED:
sessionEvent_UserJoined(event.GetExtension(Event_UserJoined::ext));
break;
case SessionEvent::USER_LEFT:
sessionEvent_UserLeft(event.GetExtension(Event_UserLeft::ext));
break;
case SessionEvent::GAME_JOINED: {
QReadLocker clientsLocker(&server->clientsLock);
Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId);
if (!client) {
qCDebug(IslInterfaceLog) << "IslInterface::processSessionEvent: session id" << sessionId << "not found";
break;
}
const Event_GameJoined &gameJoined = event.GetExtension(Event_GameJoined::ext);
client->playerAddedToGame(gameJoined.game_info().game_id(), gameJoined.game_info().room_id(),
gameJoined.player_id());
client->sendProtocolItem(event);
break;
}
case SessionEvent::USER_MESSAGE:
case SessionEvent::REPLAY_ADDED: {
QReadLocker clientsLocker(&server->clientsLock);
Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId);
if (!client) {
qCWarning(IslInterfaceLog)
<< "IslInterface::processSessionEvent: session id" << sessionId << "not found";
break;
}
client->sendProtocolItem(event);
break;
}
default:;
}
}
void IslInterface::processRoomEvent(const RoomEvent &event)
{
switch (getPbExtension(event)) {
case RoomEvent::JOIN_ROOM:
roomEvent_UserJoined(event.room_id(), event.GetExtension(Event_JoinRoom::ext));
break;
case RoomEvent::LEAVE_ROOM:
roomEvent_UserLeft(event.room_id(), event.GetExtension(Event_LeaveRoom::ext));
break;
case RoomEvent::ROOM_SAY:
roomEvent_Say(event.room_id(), event.GetExtension(Event_RoomSay::ext));
break;
case RoomEvent::LIST_GAMES:
roomEvent_ListGames(event.room_id(), event.GetExtension(Event_ListGames::ext));
break;
case RoomEvent::REMOVE_MESSAGES:
roomEvent_RemoveMessages(event.room_id(), event.GetExtension(Event_RemoveMessages::ext));
break;
default:;
}
}
void IslInterface::processRoomCommand(const CommandContainer &cont, qint64 sessionId)
{
for (int i = 0; i < cont.room_command_size(); ++i) {
const RoomCommand &roomCommand = cont.room_command(i);
switch (static_cast<RoomCommand::RoomCommandType>(getPbExtension(roomCommand))) {
case RoomCommand::JOIN_GAME:
roomCommand_JoinGame(roomCommand.GetExtension(Command_JoinGame::ext), cont.cmd_id(), cont.room_id(),
sessionId);
default:;
}
}
}
void IslInterface::processMessage(const IslMessage &item)
{
qCDebug(IslInterfaceLog) << getSafeDebugString(item);
switch (item.message_type()) {
case IslMessage::ROOM_COMMAND_CONTAINER: {
processRoomCommand(item.room_command(), item.session_id());
break;
}
case IslMessage::GAME_COMMAND_CONTAINER: {
emit gameCommandContainerReceived(item.game_command(), item.player_id(), serverId, item.session_id());
break;
}
case IslMessage::SESSION_EVENT: {
processSessionEvent(item.session_event(), item.session_id());
break;
}
case IslMessage::RESPONSE: {
emit responseReceived(item.response(), item.session_id());
break;
}
case IslMessage::GAME_EVENT_CONTAINER: {
emit gameEventContainerReceived(item.game_event_container(), item.session_id());
break;
}
case IslMessage::ROOM_EVENT: {
processRoomEvent(item.room_event());
break;
}
default:;
}
}
+102
View File
@@ -0,0 +1,102 @@
#ifndef ISL_INTERFACE_H
#define ISL_INTERFACE_H
#include "servatrice.h"
#include <QSslCertificate>
#include <QWaitCondition>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
class Servatrice;
class QSslSocket;
class QSslKey;
class IslMessage;
class Event_ServerCompleteList;
class Event_UserMessage;
class Event_UserJoined;
class Event_UserLeft;
class Event_JoinRoom;
class Event_LeaveRoom;
class Event_RoomSay;
class Event_ListGames;
class Event_RemoveMessages;
class Command_JoinGame;
class IslInterface : public QObject
{
Q_OBJECT
private slots:
void readClient();
void catchSocketError(QAbstractSocket::SocketError socketError);
void flushOutputBuffer();
signals:
void outputBufferChanged();
void externalUserJoined(ServerInfo_User userInfo);
void externalUserLeft(QString userName);
void externalRoomUserJoined(int roomId, ServerInfo_User userInfo);
void externalRoomUserLeft(int roomId, QString userName);
void externalRoomSay(int roomId, QString userName, QString message);
void externalRoomGameListChanged(int roomId, ServerInfo_Game gameInfo);
void externalRoomRemoveMessages(int roomId, QString userName, int amount);
void joinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId);
void gameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId);
void responseReceived(const Response &resp, qint64 sessionId);
void gameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId);
private:
int serverId;
int socketDescriptor;
QString peerHostName, peerAddress;
int peerPort;
QSslCertificate peerCert;
QMutex outputBufferMutex;
Servatrice *server;
QSslSocket *socket;
QByteArray inputBuffer, outputBuffer;
bool messageInProgress;
int messageLength;
void sessionEvent_ServerCompleteList(const Event_ServerCompleteList &event);
void sessionEvent_UserJoined(const Event_UserJoined &event);
void sessionEvent_UserLeft(const Event_UserLeft &event);
void roomEvent_UserJoined(int roomId, const Event_JoinRoom &event);
void roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event);
void roomEvent_Say(int roomId, const Event_RoomSay &event);
void roomEvent_ListGames(int roomId, const Event_ListGames &event);
void roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event);
void roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId);
void processSessionEvent(const SessionEvent &event, qint64 sessionId);
void processRoomEvent(const RoomEvent &event);
void processRoomCommand(const CommandContainer &cont, qint64 sessionId);
void processMessage(const IslMessage &item);
void sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey);
public slots:
void initServer();
void initClient();
public:
IslInterface(int socketDescriptor, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server);
IslInterface(int _serverId,
const QString &peerHostName,
const QString &peerAddress,
int peerPort,
const QSslCertificate &peerCert,
const QSslCertificate &cert,
const QSslKey &privateKey,
Servatrice *_server);
~IslInterface();
void transmitMessage(const IslMessage &item);
};
#endif
+218
View File
@@ -0,0 +1,218 @@
/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@laptop *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "servatrice.h"
#include "server_logger.h"
#include "settingscache.h"
#include "signalhandler.h"
#include "smtpclient.h"
#include "version_string.h"
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDateTime>
#include <QFile>
#include <QMetaType>
#include <QtGlobal>
#include <iostream>
#include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/utility/passwordhasher.h>
RNG_Abstract *rng;
ServerLogger *logger;
QThread *loggerThread;
SettingsCache *settingsCache;
SignalHandler *signalhandler;
SmtpClient *smtpClient;
/* Prototypes */
void testRNG();
void testHash();
void myMessageOutput(QtMsgType type, const QMessageLogContext &, const QString &msg);
void myMessageOutput2(QtMsgType type, const QMessageLogContext &, const QString &msg);
/* Implementations */
void testRNG()
{
const int n = 500000;
std::cerr << "Testing random number generator (n = " << n << " * bins)..." << std::endl;
const int min = 1;
const int minMax = 2;
const int maxMax = 10;
QVector<QVector<int>> numbers(maxMax - minMax + 1);
QVector<double> chisq(maxMax - minMax + 1);
for (int max = minMax; max <= maxMax; ++max) {
numbers[max - minMax] = rng->makeNumbersVector(n * (max - min + 1), min, max);
chisq[max - minMax] = rng->testRandom(numbers[max - minMax]);
}
for (int i = 0; i <= maxMax - min; ++i) {
std::cerr << (min + i);
for (auto &number : numbers) {
if (i < number.size()) {
std::cerr << "\t" << number[i];
} else {
std::cerr << "\t";
}
}
std::cerr << std::endl;
}
std::cerr << std::endl << "Chi^2 =";
for (double j : chisq) {
std::cerr << "\t" << QString::number(j, 'f', 3).toStdString();
}
std::cerr << std::endl << "k =";
for (int j = 0; j < chisq.size(); ++j) {
std::cerr << "\t" << (j - min + minMax);
}
std::cerr << std::endl << std::endl;
}
void testHash()
{
const int n = 5000;
std::cerr << "Benchmarking password hash function (n =" << n << ")..." << std::endl;
QDateTime startTime = QDateTime::currentDateTime();
for (int i = 0; i < n; ++i) {
PasswordHasher::computeHash("aaaaaa", "aaaaaaaaaaaaaaaa");
}
QDateTime endTime = QDateTime::currentDateTime();
std::cerr << startTime.secsTo(endTime) << "secs" << std::endl;
}
void myMessageOutput(QtMsgType /*type*/, const QMessageLogContext &, const QString &msg)
{
logger->logMessage(msg);
}
void myMessageOutput2(QtMsgType /*type*/, const QMessageLogContext &, const QString &msg)
{
logger->logMessage(msg);
std::cerr << msg.toStdString() << std::endl;
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setOrganizationName("Cockatrice");
QCoreApplication::setApplicationName("Servatrice");
QCoreApplication::setApplicationVersion(VERSION_STRING);
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption testRandomOpt("test-random", "Test PRNG (chi^2)");
parser.addOption(testRandomOpt);
QCommandLineOption testHashFunctionOpt("test-hash", "Test password hash function");
parser.addOption(testHashFunctionOpt);
QCommandLineOption logToConsoleOpt("log-to-console", "Write server logs to console");
parser.addOption(logToConsoleOpt);
QCommandLineOption configPathOpt("config", "Read server configuration from <file>", "file", "");
parser.addOption(configPathOpt);
parser.process(app);
bool testRandom = parser.isSet(testRandomOpt);
bool testHashFunction = parser.isSet(testHashFunctionOpt);
bool logToConsole = parser.isSet(logToConsoleOpt);
QString configPath = parser.value(configPathOpt);
qRegisterMetaType<QList<int>>("QList<int>");
if (configPath.isEmpty()) {
configPath = SettingsCache::guessConfigurationPath();
} else if (!QFile::exists(configPath)) {
qCritical() << "Could not find configuration file at" << configPath;
return 1;
}
qWarning() << "Using configuration file: " << configPath;
settingsCache = new SettingsCache(configPath);
loggerThread = new QThread;
loggerThread->setObjectName("logger");
logger = new ServerLogger(logToConsole);
logger->moveToThread(loggerThread);
loggerThread->start();
QMetaObject::invokeMethod(logger, "startLog", Qt::BlockingQueuedConnection,
Q_ARG(QString, settingsCache->value("server/logfile", QString("server.log")).toString()));
if (logToConsole) {
qInstallMessageHandler(myMessageOutput);
} else {
qInstallMessageHandler(myMessageOutput2);
}
signalhandler = new SignalHandler();
rng = new RNG_SFMT;
std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl;
std::cerr << "-------------------------" << std::endl;
if (testRandom) {
testRNG();
}
if (testHashFunction) {
testHash();
}
if (testRandom || testHashFunction) {
return 0;
}
smtpClient = new SmtpClient();
auto *server = new Servatrice();
QObject::connect(server, SIGNAL(destroyed()), &app, SLOT(quit()), Qt::QueuedConnection);
int retval = 0;
if (server->initServer()) {
std::cerr << "-------------------------" << std::endl;
std::cerr << "Server initialized." << std::endl;
qInstallMessageHandler(myMessageOutput);
retval = QCoreApplication::exec();
std::cerr << "Server quit." << std::endl;
std::cerr << "-------------------------" << std::endl;
}
delete smtpClient;
delete rng;
delete signalhandler;
delete settingsCache;
logger->deleteLater();
loggerThread->wait();
delete loggerThread;
// Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
QCoreApplication::quit();
return retval;
}
+14
View File
@@ -0,0 +1,14 @@
#ifndef MAIN_H
#define MAIN_H
class ServerLogger;
class QThread;
class SettingsCache;
class SmtpClient;
extern ServerLogger *logger;
extern QThread *loggerThread;
extern SettingsCache *settingsCache;
extern SmtpClient *smtpClient;
#endif
File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@laptop *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#ifndef SERVATRICE_H
#define SERVATRICE_H
#include <QHostAddress>
#include <QMetaType>
#include <QMutex>
#include <QReadWriteLock>
#include <QSqlDatabase>
#include <QSslCertificate>
#include <QSslKey>
#include <QTcpServer>
#include <QWebSocketServer>
#include <server.h>
#include <utility>
Q_DECLARE_METATYPE(QSqlDatabase)
class QSqlQuery;
class QTimer;
class GameReplay;
class Servatrice;
class Servatrice_ConnectionPool;
class Servatrice_DatabaseInterface;
class AbstractServerSocketInterface;
class IslInterface;
class FeatureSet;
class Servatrice_GameServer : public QTcpServer
{
Q_OBJECT
private:
Servatrice *server;
QList<Servatrice_ConnectionPool *> connectionPools;
public:
Servatrice_GameServer(Servatrice *_server,
int _numberPools,
const QSqlDatabase &_sqlDatabase,
QObject *parent = nullptr);
~Servatrice_GameServer() override;
protected:
void incomingConnection(qintptr socketDescriptor) override;
Servatrice_ConnectionPool *findLeastUsedConnectionPool();
};
class Servatrice_WebsocketGameServer : public QWebSocketServer
{
Q_OBJECT
private:
Servatrice *server;
QList<Servatrice_ConnectionPool *> connectionPools;
public:
Servatrice_WebsocketGameServer(Servatrice *_server,
int _numberPools,
const QSqlDatabase &_sqlDatabase,
QObject *parent = nullptr);
~Servatrice_WebsocketGameServer() override;
protected:
Servatrice_ConnectionPool *findLeastUsedConnectionPool();
protected slots:
void onNewConnection();
};
class Servatrice_IslServer : public QTcpServer
{
Q_OBJECT
private:
Servatrice *server;
QSslCertificate cert;
QSslKey privateKey;
public:
Servatrice_IslServer(Servatrice *_server,
const QSslCertificate &_cert,
QSslKey _privateKey,
QObject *parent = nullptr)
: QTcpServer(parent), server(_server), cert(_cert), privateKey(std::move(_privateKey))
{
}
protected:
void incomingConnection(qintptr socketDescriptor) override;
};
class ServerProperties
{
public:
int id;
QSslCertificate cert;
QString hostname;
QHostAddress address;
int gamePort;
int controlPort;
ServerProperties(int _id,
const QSslCertificate &_cert,
QString _hostname,
const QHostAddress &_address,
int _gamePort,
int _controlPort)
: id(_id), cert(_cert), hostname(std::move(_hostname)), address(_address), gamePort(_gamePort),
controlPort(_controlPort)
{
}
};
class Servatrice : public Server
{
Q_OBJECT
public:
enum AuthenticationMethod
{
AuthenticationNone,
AuthenticationSql,
AuthenticationPassword
};
private slots:
void statusUpdate();
void shutdownTimeout();
protected:
void doSendIslMessage(const IslMessage &msg, int _serverId) override;
private:
enum DatabaseType
{
DatabaseNone,
DatabaseMySql
};
AuthenticationMethod authenticationMethod;
DatabaseType databaseType;
QTimer *pingClock, *statusUpdateClock;
Servatrice_GameServer *gameServer;
Servatrice_WebsocketGameServer *websocketGameServer;
Servatrice_IslServer *islServer;
mutable QMutex loginMessageMutex;
QString loginMessage;
QString dbPrefix;
QMap<QString, bool> serverRequiredFeatureList;
QString officialWarnings;
Servatrice_DatabaseInterface *servatriceDatabaseInterface;
int serverId;
int uptime;
QMutex txBytesMutex, rxBytesMutex;
quint64 txBytes, rxBytes;
QString shutdownReason;
int shutdownMinutes;
int nextShutdownMessageMinutes;
QTimer *shutdownTimer;
mutable QMutex serverListMutex;
QList<ServerProperties> serverList;
void updateServerList();
QMap<int, IslInterface *> islInterfaces;
QString getDBPrefixString() const;
QString getDBHostNameString() const;
QString getDBDatabaseNameString() const;
QString getDBUserNameString() const;
QString getDBPasswordString() const;
QString getRoomsMethodString() const;
QString getISLNetworkSSLCertFile() const;
QString getISLNetworkSSLKeyFile() const;
int getServerStatusUpdateTime() const;
int getNumberOfTCPPools() const;
int getServerTCPPort() const;
int getNumberOfWebSocketPools() const;
int getServerWebSocketPort() const;
int getISLNetworkPort() const;
bool getISLNetworkEnabled() const;
bool getEnableInternalSMTPClient() const;
QHostAddress getServerTCPHost() const;
QHostAddress getServerWebSocketHost() const;
public slots:
void scheduleShutdown(const QString &reason, int minutes);
void updateLoginMessage();
void setRequiredFeatures(const QString &featureList);
public:
explicit Servatrice(QObject *parent = nullptr);
~Servatrice() override;
bool initServer();
QMap<QString, bool> getServerRequiredFeatureList() const override
{
return serverRequiredFeatureList;
}
QString getServerName() const;
QString getLoginMessage() const override
{
QMutexLocker locker(&loginMessageMutex);
return loginMessage;
}
QString getRequiredFeatures() const override;
QString getAuthenticationMethodString() const;
QString getDBTypeString() const;
QString getDbPrefix() const
{
return dbPrefix;
}
QString getEmailBlackList() const;
QString getEmailWhiteList() const;
AuthenticationMethod getAuthenticationMethod() const
{
return authenticationMethod;
}
bool permitUnregisteredUsers() const override
{
return authenticationMethod != AuthenticationNone;
}
bool getGameShouldPing() const override
{
return true;
}
bool getClientIDRequiredEnabled() const override;
bool getRegOnlyServerEnabled() const override;
bool getMaxUserLimitEnabled() const override;
bool getStoreReplaysEnabled() const override;
bool getRegistrationEnabled() const;
bool getRequireEmailForRegistrationEnabled() const;
bool getRequireEmailActivationEnabled() const;
bool getEnableLogQuery() const override;
bool getEnableForgotPassword() const;
bool getEnableForgotPasswordChallenge() const;
bool getEnableAudit() const;
bool getEnableRegistrationAudit() const;
bool getEnableForgotPasswordAudit() const;
int getMinPasswordLength() const;
int getIdleClientTimeout() const override;
int getServerID() const override;
int getMaxGameInactivityTime() const override;
int getMaxPlayerInactivityTime() const override;
int getClientKeepAlive() const override;
int getMaxUsersPerAddress() const;
int getMessageCountingInterval() const override;
int getMaxMessageCountPerInterval() const override;
int getMaxMessageSizePerInterval() const override;
int getMaxGamesPerUser() const override;
int getCommandCountingInterval() const override;
int getMaxCommandCountPerInterval() const override;
int getMaxUserTotal() const override;
bool permitCreateGameAsJudge() const override;
int getMaxTcpUserLimit() const;
int getMaxWebSocketUserLimit() const;
int getUsersWithAddress(const QHostAddress &address) const;
int getMaxAccountsPerEmail() const;
int getForgotPasswordTokenLife() const;
QList<AbstractServerSocketInterface *> getUsersWithAddressAsList(const QHostAddress &address) const;
void incTxBytes(quint64 num);
void incRxBytes(quint64 num);
void addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface);
bool islConnectionExists(int _serverId) const;
void addIslInterface(int _serverId, IslInterface *interface);
void removeIslInterface(int _serverId);
QReadWriteLock islLock;
QList<ServerProperties> getServerList() const;
};
#endif
@@ -0,0 +1,16 @@
#include "servatrice_connection_pool.h"
#include "servatrice_database_interface.h"
#include <QThread>
Servatrice_ConnectionPool::Servatrice_ConnectionPool(Servatrice_DatabaseInterface *_databaseInterface)
: databaseInterface(_databaseInterface), threaded(false), clientCount(0)
{
}
Servatrice_ConnectionPool::~Servatrice_ConnectionPool()
{
delete databaseInterface;
thread()->quit();
}
@@ -0,0 +1,46 @@
#ifndef SERVATRICE_CONNECTION_POOL_H
#define SERVATRICE_CONNECTION_POOL_H
#include <QMutex>
#include <QMutexLocker>
#include <QObject>
class Servatrice_DatabaseInterface;
class Servatrice_ConnectionPool : public QObject
{
Q_OBJECT
private:
Servatrice_DatabaseInterface *databaseInterface;
bool threaded;
mutable QMutex clientCountMutex;
int clientCount;
public:
explicit Servatrice_ConnectionPool(Servatrice_DatabaseInterface *_databaseInterface);
~Servatrice_ConnectionPool() override;
Servatrice_DatabaseInterface *getDatabaseInterface() const
{
return databaseInterface;
}
int getClientCount() const
{
QMutexLocker locker(&clientCountMutex);
return clientCount;
}
void addClient()
{
QMutexLocker locker(&clientCountMutex);
++clientCount;
}
public slots:
void removeClient()
{
QMutexLocker locker(&clientCountMutex);
--clientCount;
}
};
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,152 @@
#ifndef SERVATRICE_DATABASE_INTERFACE_H
#define SERVATRICE_DATABASE_INTERFACE_H
#include <QChar>
#include <QHash>
#include <QObject>
#include <QSqlDatabase>
#include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_warning.pb.h>
#include <server.h>
#include <server_database_interface.h>
#define DATABASE_SCHEMA_VERSION 35
class Servatrice;
class Servatrice_DatabaseInterface : public Server_DatabaseInterface
{
Q_OBJECT
private:
int instanceId;
QSqlDatabase sqlDatabase;
QHash<QString, QSqlQuery *> preparedStatements;
Servatrice *server;
ServerInfo_User evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId = false);
/** Must be called after checkSql and server is known to be in auth mode. */
bool checkUserIsIdBanned(const QString &clientId, QString &banReason, int &banSecondsRemaining);
/** Must be called after checkSql and server is known to be in auth mode. */
bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining);
/** Must be called after checkSql and server is known to be in auth mode. */
bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining);
protected:
AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler,
const QString &user,
const QString &password,
const QString &clientId,
QString &reasonStr,
int &banSecondsLeft,
bool passwordNeedsHash) override;
public slots:
void initDatabase(const QSqlDatabase &_sqlDatabase);
public:
explicit Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server);
~Servatrice_DatabaseInterface() override;
bool initDatabase(const QString &type,
const QString &hostName,
const QString &databaseName,
const QString &userName,
const QString &password);
bool openDatabase();
bool checkSql();
QSqlQuery *prepareQuery(const QString &queryText);
bool execSqlQuery(QSqlQuery *query);
const QSqlDatabase &getDatabase()
{
return sqlDatabase;
}
bool activeUserExists(const QString &user) override;
bool userExists(const QString &user) override;
QString getUserSalt(const QString &user) override;
int getUserIdInDB(const QString &name);
QMap<QString, ServerInfo_User> getBuddyList(const QString &name) override;
QMap<QString, ServerInfo_User> getIgnoreList(const QString &name) override;
bool isInBuddyList(const QString &whoseList, const QString &who) override;
bool isInIgnoreList(const QString &whoseList, const QString &who) override;
ServerInfo_User getUserData(const QString &name, bool withId = false) override;
void storeGameInformation(const QString &roomName,
const QStringList &roomGameTypes,
const ServerInfo_Game &gameInfo,
const QSet<QString> &allPlayersEver,
const QSet<QString> &allSpectatorsEver,
const QList<GameReplay *> &replayList) override;
DeckList *getDeckFromDatabase(int deckId, int userId) override;
int getNextGameId() override;
int getNextReplayId() override;
int getActiveUserCount(QString connectionType = QString()) override;
qint64 startSession(const QString &userName,
const QString &address,
const QString &clientId,
const QString &connectionType) override;
void endSession(qint64 sessionId) override;
void clearSessionTables() override;
void lockSessionTables() override;
void unlockSessionTables() override;
bool userSessionExists(const QString &userName) override;
bool usernameIsValid(const QString &user, QString &error) override;
bool checkUserIsBanned(const QString &ipAddress,
const QString &userName,
const QString &clientId,
QString &banReason,
int &banSecondsRemaining) override;
int checkNumberOfUserAccounts(const QString &email) override;
bool registerUser(const QString &userName,
const QString &realName,
const QString &password,
bool passwordNeedsHash,
const QString &emailAddress,
const QString &country,
bool active = false) override;
bool activateUser(const QString &userName, const QString &token) override;
void updateUsersClientID(const QString &userName, const QString &userClientID) override;
void updateUsersLastLoginData(const QString &userName, const QString &clientVersion) override;
void logMessage(const int senderId,
const QString &senderName,
const QString &senderIp,
const QString &logMessage,
LogMessage_TargetType targetType,
const int targetId,
const QString &targetName) override;
bool changeUserPassword(const QString &user, const QString &password, bool passwordNeedsHash) override;
bool changeUserPassword(const QString &user,
const QString &oldPassword,
bool oldPasswordNeedsHash,
const QString &newPassword,
bool newPasswordNeedsHash) override;
QList<ServerInfo_Ban> getUserBanHistory(const QString userName);
bool
addWarning(const QString userName, const QString adminName, const QString warningReason, const QString clientID);
QList<ServerInfo_Warning> getUserWarnHistory(const QString userName);
QList<ServerInfo_ChatMessage> getMessageLogHistory(const QString &user,
const QString &ipaddress,
const QString &gamename,
const QString &gameid,
const QString &message,
bool &chat,
bool &game,
bool &room,
int &range,
int &maxresults);
bool addForgotPassword(const QString &user);
bool removeForgotPassword(const QString &user) override;
bool doesForgotPasswordExist(const QString &user);
bool updateUserToken(const QString &token, const QString &user);
bool validateTableColumnStringData(const QString &table,
const QString &column,
const QString &_user,
const QString &_datatocheck);
void addAuditRecord(const QString &user,
const QString &ipaddress,
const QString &clientid,
const QString &action,
const QString &details,
const bool &results);
};
#endif
+132
View File
@@ -0,0 +1,132 @@
#include "server_logger.h"
#include "settingscache.h"
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <iostream>
ServerLogger::ServerLogger(bool _logToConsole, QObject *parent)
: QObject(parent), logToConsole(_logToConsole), flushRunning(false)
{
}
ServerLogger::~ServerLogger()
{
flushBuffer();
// This does not work with the destroyed() signal as this destructor is called after the main event loop is done.
thread()->quit();
}
void ServerLogger::startLog(const QString &logFileName)
{
if (!logFileName.isEmpty()) {
QFileInfo fi(logFileName);
QDir fileDir(fi.path());
if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) {
std::cerr << "ERROR: logfile folder doesn't exist and i can't create it." << std::endl;
logFile = 0;
return;
}
logFile = new QFile(logFileName, this);
if (!logFile->open(QIODevice::Append)) {
std::cerr << "ERROR: can't open() logfile." << std::endl;
delete logFile;
logFile = 0;
return;
}
} else {
logFile = 0;
}
connect(this, SIGNAL(sigFlushBuffer()), this, SLOT(flushBuffer()), Qt::QueuedConnection);
}
void ServerLogger::logMessage(const QString &message, void *caller)
{
if (!logFile) {
return;
}
QString callerString;
if (caller) {
callerString = QString::number((qulonglong)caller, 16) + " ";
}
// filter out all log entries based on values in configuration file
bool shouldWeWriteLog = settingsCache->value("server/writelog", 1).toBool();
QString logFilters = settingsCache->value("server/logfilters").toString();
QStringList listlogFilters = logFilters.split(",", Qt::SkipEmptyParts);
bool shouldWeSkipLine = false;
if (!shouldWeWriteLog) {
return;
}
if (!logFilters.trimmed().isEmpty()) {
shouldWeSkipLine = true;
for (const QString &logFilter : listlogFilters) {
if (message.contains(logFilter, Qt::CaseInsensitive)) {
shouldWeSkipLine = false;
break;
}
}
}
if (shouldWeSkipLine) {
return;
}
bufferMutex.lock();
buffer.append(QDateTime::currentDateTime().toString() + " " + callerString + message);
bufferMutex.unlock();
emit sigFlushBuffer();
}
void ServerLogger::flushBuffer()
{
if (flushRunning) {
return;
}
flushRunning = true;
QTextStream stream(logFile);
forever
{
bufferMutex.lock();
if (buffer.isEmpty()) {
bufferMutex.unlock();
flushRunning = false;
return;
}
QString message = buffer.takeFirst();
bufferMutex.unlock();
stream << message << "\n";
stream.flush();
if (logToConsole) {
std::cout << message.toStdString() << std::endl;
}
}
}
void ServerLogger::rotateLogs()
{
if (!logFile) {
return;
}
flushBuffer();
logFile->close();
if (!logFile->open(QIODevice::Append)) {
std::cerr << "ERROR: Failed to open log file for writing!" << std::endl;
}
}
QFile *ServerLogger::logFile;
+36
View File
@@ -0,0 +1,36 @@
#ifndef SERVER_LOGGER_H
#define SERVER_LOGGER_H
#include <QMutex>
#include <QObject>
#include <QStringList>
#include <QThread>
#include <QWaitCondition>
class QFile;
class Server_ProtocolHandler;
class ServerLogger : public QObject
{
Q_OBJECT
public:
ServerLogger(bool _logToConsole, QObject *parent = 0);
~ServerLogger();
public slots:
void startLog(const QString &logFileName);
void logMessage(const QString &message, void *caller = 0);
void rotateLogs();
private slots:
void flushBuffer();
signals:
void sigFlushBuffer();
private:
bool logToConsole;
static QFile *logFile;
bool flushRunning;
QStringList buffer;
QMutex bufferMutex;
};
#endif
File diff suppressed because it is too large Load Diff
+255
View File
@@ -0,0 +1,255 @@
/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@laptop *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#ifndef SERVERSOCKETINTERFACE_H
#define SERVERSOCKETINTERFACE_H
#include <QHostAddress>
#include <QMutex>
#include <QTcpSocket>
#include <QWebSocket>
#include <server_protocolhandler.h>
class Servatrice;
class Servatrice_DatabaseInterface;
class DeckList;
class ServerInfo_DeckStorage_Folder;
class Command_AddToList;
class Command_RemoveFromList;
class Command_DeckList;
class Command_DeckNewDir;
class Command_DeckDelDir;
class Command_DeckDel;
class Command_DeckDownload;
class Command_DeckUpload;
class Command_ReplayList;
class Command_ReplayDownload;
class Command_ReplayModifyMatch;
class Command_ReplayDeleteMatch;
class Command_ReplayGetCode;
class Command_ReplaySubmitCode;
class Command_BanFromServer;
class Command_UpdateServerMessage;
class Command_ShutdownServer;
class Command_ReloadConfig;
class Command_AccountEdit;
class Command_AccountImage;
class Command_AccountPassword;
class AbstractServerSocketInterface : public Server_ProtocolHandler
{
Q_OBJECT
protected slots:
void catchSocketError(QAbstractSocket::SocketError socketError);
void catchSocketDisconnected();
virtual void flushOutputQueue() = 0;
signals:
void outputQueueChanged();
void incTxBytes(qint64 amount);
protected:
void logDebugMessage(const QString &message);
bool tooManyRegistrationAttempts(const QString &ipAddress);
virtual void writeToSocket(QByteArray &data) = 0;
virtual void flushSocket() = 0;
Servatrice *servatrice;
QList<ServerMessage> outputQueue;
QMutex outputQueueMutex;
private:
Servatrice_DatabaseInterface *sqlInterface;
Response::ResponseCode cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc);
Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc);
int getDeckPathId(int basePathId, QStringList path);
int getDeckPathId(const QString &path);
bool deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder);
Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc);
Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc);
void deckDelDirHelper(int basePathId);
void sendServerMessage(const QString userName, const QString message);
Response::ResponseCode cmdDeckDelDir(const Command_DeckDelDir &cmd, ResponseContainer &rc);
Response::ResponseCode cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer &rc);
Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc);
DeckList *getDeckFromDatabase(int deckId);
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer &rc);
QString createHashForReplay(int gameId);
Response::ResponseCode cmdReplayGetCode(const Command_ReplayGetCode &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer &rc);
Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc);
Response::ResponseCode cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetBanHistory(const Command_GetBanHistory &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetWarnList(const Command_GetWarnList &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetWarnHistory(const Command_GetWarnHistory &cmd, ResponseContainer &rc);
Response::ResponseCode cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer &rc);
Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc);
Response::ResponseCode cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc);
Response::ResponseCode cmdActivateAccount(const Command_Activate &cmd, ResponseContainer & /* rc */);
Response::ResponseCode cmdReloadConfig(const Command_ReloadConfig & /* cmd */, ResponseContainer & /*rc*/);
Response::ResponseCode cmdAdjustMod(const Command_AdjustMod &cmd, ResponseContainer & /*rc*/);
Response::ResponseCode cmdForgotPasswordRequest(const Command_ForgotPasswordRequest &cmd, ResponseContainer &rc);
Response::ResponseCode continuePasswordRequest(const QString &userName,
const QString &clientId,
ResponseContainer &rc,
bool challenged = false);
Response::ResponseCode cmdForgotPasswordReset(const Command_ForgotPasswordReset &cmd, ResponseContainer &rc);
Response::ResponseCode cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd,
ResponseContainer &rc);
Response::ResponseCode cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd, ResponseContainer &rc);
Response::ResponseCode processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc);
Response::ResponseCode
processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc);
Response::ResponseCode processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc);
Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc);
Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc);
bool isCardNameAllowed(const QString &cardName, const QString &cardProviderId);
Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &);
Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &);
Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &);
Response::ResponseCode cmdListCardArtRules(const Command_ListCardArtRules &, ResponseContainer &rc);
Response::ResponseCode cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer &rc);
Response::ResponseCode cmdForceActivateUser(const Command_ForceActivateUser &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetAdminNotes(const Command_GetAdminNotes &cmd, ResponseContainer &rc);
Response::ResponseCode cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, ResponseContainer &rc);
bool addAdminFlagToUser(const QString &user, int flag);
bool removeAdminFlagFromUser(const QString &user, int flag);
bool isPasswordLongEnough(const int passwordLength);
void removeSaidMessages(const QString &userName, int amount);
public:
AbstractServerSocketInterface(Servatrice *_server,
Servatrice_DatabaseInterface *_databaseInterface,
QObject *parent = 0);
~AbstractServerSocketInterface()
{
}
bool initSession();
virtual QHostAddress getPeerAddress() const = 0;
virtual QString getAddress() const = 0;
void transmitProtocolItem(const ServerMessage &item);
};
class TcpServerSocketInterface : public AbstractServerSocketInterface
{
Q_OBJECT
public:
TcpServerSocketInterface(Servatrice *_server,
Servatrice_DatabaseInterface *_databaseInterface,
QObject *parent = 0);
~TcpServerSocketInterface();
QHostAddress getPeerAddress() const
{
return socket->peerAddress();
}
QString getAddress() const
{
return socket->peerAddress().toString();
}
QString getConnectionType() const
{
return "tcp";
}
private:
QTcpSocket *socket;
QByteArray inputBuffer;
bool messageInProgress;
bool handshakeStarted;
int messageLength;
protected:
void writeToSocket(QByteArray &data)
{
socket->write(data);
}
void flushSocket()
{
socket->flush();
}
void initSessionDeprecated();
bool initTcpSession();
protected slots:
void readClient();
void flushOutputQueue();
public slots:
void initConnection(int socketDescriptor);
};
class WebsocketServerSocketInterface : public AbstractServerSocketInterface
{
Q_OBJECT
public:
WebsocketServerSocketInterface(Servatrice *_server,
Servatrice_DatabaseInterface *_databaseInterface,
QObject *parent = nullptr);
~WebsocketServerSocketInterface();
QHostAddress getPeerAddress() const
{
return address;
}
QString getAddress() const
{
return address.toString();
}
QString getConnectionType() const
{
return "websocket";
}
private:
QWebSocket *socket;
QHostAddress address;
protected:
void writeToSocket(QByteArray &data)
{
socket->sendBinaryMessage(data);
}
void flushSocket()
{
socket->flush();
}
bool initWebsocketSession();
protected slots:
void binaryMessageReceived(const QByteArray &message);
void flushOutputQueue();
public slots:
void initConnection(void *_socket);
};
#endif
+47
View File
@@ -0,0 +1,47 @@
#include "settingscache.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QStandardPaths>
SettingsCache::SettingsCache(const QString &fileName, QSettings::Format format, QObject *parent)
: QSettings(fileName, format, parent)
{
// first, figure out if we are running in portable mode
isPortableBuild = QFile::exists(qApp->applicationDirPath() + "/portable.dat");
QStringList disallowedRegExpStr = value("users/disallowedregexp", "").toString().split(",", Qt::SkipEmptyParts);
disallowedRegExpStr.removeDuplicates();
for (const QString &regExpStr : disallowedRegExpStr) {
disallowedRegExp.append(QRegularExpression(QString("\\A%1\\z").arg(regExpStr)));
}
}
QString SettingsCache::guessConfigurationPath()
{
const QString fileName = "servatrice.ini";
if (QFile::exists(qApp->applicationDirPath() + "/portable.dat")) {
qDebug() << "Portable mode enabled";
return fileName;
}
QString guessFileName;
// application directory path
guessFileName = QCoreApplication::applicationDirPath() + "/" + fileName;
if (QFile::exists(guessFileName)) {
return guessFileName;
}
#ifdef Q_OS_UNIX
// /etc
guessFileName = "/etc/servatrice/" + fileName;
if (QFile::exists(guessFileName)) {
return guessFileName;
}
#endif
guessFileName = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + fileName;
return guessFileName;
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef SERVATRICE_SETTINGSCACHE_H
#define SERVATRICE_SETTINGSCACHE_H
#include <QList>
#include <QRegularExpression>
#include <QSettings>
#include <QString>
class SettingsCache : public QSettings
{
Q_OBJECT
private:
bool isPortableBuild;
public:
SettingsCache(const QString &fileName = "servatrice.ini",
QSettings::Format format = QSettings::IniFormat,
QObject *parent = 0);
static QString guessConfigurationPath();
QList<QRegularExpression> disallowedRegExp;
bool getIsPortableBuild() const
{
return isPortableBuild;
}
};
extern SettingsCache *settingsCache;
#endif
+105
View File
@@ -0,0 +1,105 @@
#include "signalhandler.h"
#include "main.h"
#include "server_logger.h"
#include "settingscache.h"
#include <QSocketNotifier>
#ifdef Q_OS_UNIX
#include <cstdio>
#include <execinfo.h>
#include <iostream>
#include <signal.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#define SIGSEGV_TRACE_LINES 40
int SignalHandler::sigHupFD[2];
SignalHandler::SignalHandler(QObject *parent) : QObject(parent), snHup(nullptr)
{
#ifdef Q_OS_UNIX
::socketpair(AF_UNIX, SOCK_STREAM, 0, sigHupFD);
snHup = new QSocketNotifier(sigHupFD[1], QSocketNotifier::Read, this);
connect(snHup, SIGNAL(activated(int)), this, SLOT(internalSigHupHandler()));
struct sigaction hup;
hup.sa_handler = SignalHandler::sigHupHandler;
sigemptyset(&hup.sa_mask);
hup.sa_flags = 0;
hup.sa_flags |= SA_RESTART;
sigaction(SIGHUP, &hup, 0);
struct sigaction segv;
segv.sa_handler = SignalHandler::sigSegvHandler;
segv.sa_flags = SA_RESETHAND;
sigemptyset(&segv.sa_mask);
sigaction(SIGSEGV, &segv, 0);
sigaction(SIGABRT, &segv, 0);
signal(SIGPIPE, SIG_IGN);
#endif
}
void SignalHandler::sigHupHandler(int /* sig */)
{
#ifdef Q_OS_UNIX
char a = 1;
ssize_t writeValue = ::write(sigHupFD[0], &a, sizeof(a));
Q_UNUSED(writeValue);
#endif
}
void SignalHandler::internalSigHupHandler()
{
snHup->setEnabled(false);
#ifdef Q_OS_UNIX
char tmp;
ssize_t readValue = ::read(sigHupFD[1], &tmp, sizeof(tmp));
Q_UNUSED(readValue);
std::cerr << "Received SIGHUP" << std::endl;
#endif
logger->logMessage("Received SIGHUP, rotating logs and reloading configuration", this);
logger->rotateLogs();
settingsCache->sync();
snHup->setEnabled(true);
}
#ifdef Q_OS_UNIX
void SignalHandler::sigSegvHandler(int sig)
{
void *array[SIGSEGV_TRACE_LINES];
size_t size;
// get void*'s for all entries on the stack
size = backtrace(array, SIGSEGV_TRACE_LINES);
// print out all the frames to stderr
fprintf(stderr, "Error: signal %d:\n", sig);
backtrace_symbols_fd(array, size, STDERR_FILENO);
if (sig == SIGSEGV) {
logger->logMessage("CRASH: SIGSEGV");
} else if (sig == SIGABRT) {
logger->logMessage("CRASH: SIGABRT");
}
logger->deleteLater();
loggerThread->wait();
delete loggerThread;
raise(sig);
}
#else
void SignalHandler::sigSegvHandler(int /* sig */)
{
}
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SIGNALHANDLER_H
#define SIGNALHANDLER_H
#include <QObject>
class QSocketNotifier;
class SignalHandler : public QObject
{
Q_OBJECT
public:
SignalHandler(QObject *parent = 0);
~SignalHandler()
{
}
static void sigHupHandler(int /* sig */);
static void sigSegvHandler(int sig);
private:
static int sigHupFD[2];
QSocketNotifier *snHup;
private slots:
void internalSigHupHandler();
};
#endif
+208
View File
@@ -0,0 +1,208 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtCore module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTGLOBAL_H
#define QXTGLOBAL_H
#include <QtGlobal>
#define QXT_VERSION 0x000602
#define QXT_VERSION_STR "0.6.2"
#define QXT_STATIC
//--------------------------global macros------------------------------
#ifndef QXT_NO_MACROS
#endif // QXT_NO_MACROS
//--------------------------export macros------------------------------
#define QXT_DLLEXPORT DO_NOT_USE_THIS_ANYMORE
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_CORE)
# define QXT_CORE_EXPORT Q_DECL_EXPORT
# else
# define QXT_CORE_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_CORE_EXPORT
#endif // BUILD_QXT_CORE
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_GUI)
# define QXT_GUI_EXPORT Q_DECL_EXPORT
# else
# define QXT_GUI_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_GUI_EXPORT
#endif // BUILD_QXT_GUI
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_NETWORK)
# define QXT_NETWORK_EXPORT Q_DECL_EXPORT
# else
# define QXT_NETWORK_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_NETWORK_EXPORT
#endif // BUILD_QXT_NETWORK
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_SQL)
# define QXT_SQL_EXPORT Q_DECL_EXPORT
# else
# define QXT_SQL_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_SQL_EXPORT
#endif // BUILD_QXT_SQL
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_WEB)
# define QXT_WEB_EXPORT Q_DECL_EXPORT
# else
# define QXT_WEB_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_WEB_EXPORT
#endif // BUILD_QXT_WEB
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_BERKELEY)
# define QXT_BERKELEY_EXPORT Q_DECL_EXPORT
# else
# define QXT_BERKELEY_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_BERKELEY_EXPORT
#endif // BUILD_QXT_BERKELEY
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_ZEROCONF)
# define QXT_ZEROCONF_EXPORT Q_DECL_EXPORT
# else
# define QXT_ZEROCONF_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_ZEROCONF_EXPORT
#endif // QXT_ZEROCONF_EXPORT
#if defined BUILD_QXT_CORE || defined BUILD_QXT_GUI || defined BUILD_QXT_SQL || defined BUILD_QXT_NETWORK || defined BUILD_QXT_WEB || defined BUILD_QXT_BERKELEY || defined BUILD_QXT_ZEROCONF
# define BUILD_QXT
#endif
QXT_CORE_EXPORT const char* qxtVersion();
#ifndef QT_BEGIN_NAMESPACE
#define QT_BEGIN_NAMESPACE
#endif
#ifndef QT_END_NAMESPACE
#define QT_END_NAMESPACE
#endif
#ifndef QT_FORWARD_DECLARE_CLASS
#define QT_FORWARD_DECLARE_CLASS(Class) class Class;
#endif
/****************************************************************************
** This file is derived from code bearing the following notice:
** The sole author of this file, Adam Higerd, has explicitly disclaimed all
** copyright interest and protection for the content within. This file has
** been placed in the public domain according to United States copyright
** statute and case law. In jurisdictions where this public domain dedication
** is not legally recognized, anyone who receives a copy of this file is
** permitted to use, modify, duplicate, and redistribute this file, in whole
** or in part, with no restrictions or conditions. In these jurisdictions,
** this file shall be copyright (C) 2006-2008 by Adam Higerd.
****************************************************************************/
#define QXT_DECLARE_PRIVATE(PUB) friend class PUB##Private; QxtPrivateInterface<PUB, PUB##Private> qxt_d;
#define QXT_DECLARE_PUBLIC(PUB) friend class PUB;
#define QXT_INIT_PRIVATE(PUB) qxt_d.setPublic(this);
#define QXT_D(PUB) PUB##Private& d = qxt_d()
#define QXT_P(PUB) PUB& p = qxt_p()
template <typename PUB>
class QxtPrivate
{
public:
virtual ~QxtPrivate()
{}
inline void QXT_setPublic(PUB* pub)
{
qxt_p_ptr = pub;
}
protected:
inline PUB& qxt_p()
{
return *qxt_p_ptr;
}
inline const PUB& qxt_p() const
{
return *qxt_p_ptr;
}
private:
PUB* qxt_p_ptr;
};
template <typename PUB, typename PVT>
class QxtPrivateInterface
{
friend class QxtPrivate<PUB>;
public:
QxtPrivateInterface()
{
pvt = new PVT;
}
~QxtPrivateInterface()
{
delete pvt;
}
inline void setPublic(PUB* pub)
{
pvt->QXT_setPublic(pub);
}
inline PVT& operator()()
{
return *static_cast<PVT*>(pvt);
}
inline const PVT& operator()() const
{
return *static_cast<PVT*>(pvt);
}
private:
QxtPrivateInterface(const QxtPrivateInterface&) { }
QxtPrivateInterface& operator=(const QxtPrivateInterface&) { }
QxtPrivate<PUB>* pvt;
};
#endif // QXT_GLOBAL
+210
View File
@@ -0,0 +1,210 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtCore module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#include "qxthmac.h"
#include <QtGlobal>
/*
\class QxtHmac
\inmodule QxtCore
\brief The QxtHmac class calculates keyed-Hash Message Authentication Codes
HMAC is a well-known algorithm for generating a message authentication code (MAC) that can be used to verify the
integrity and authenticity of a message.
This class requires Qt 4.3.0 or greater.
To verify a message, the sender creates a MAC using a key, which is a secret known only to the sender and recipient,
and the content of the message. This MAC is then sent along with the message. The recipient then creates another MAC
using the shared key and the content of the message. If the two codes match, the message is verified.
HMAC has been used as a password encryption scheme. The final output of the HMAC algorithm depends on the shared key
and an inner hash. This inner hash is generated from the message content and the key. To use HMAC as a password
scheme, the key should be the username; the message should be the user's password. The authenticating party (for
instance, a login server) only needs to store this inner hash generated by the innerHash() function. When requesting
authentication, the user calculates a HMAC using this key and message and sends his username and this HMAC to the
authenticator. The authenticator can then use verify() using the provided HMAC and the stored inner hash. When using
this scheme, the password is never stored or transmitted in plain text.
*/
#ifndef QXT_DOXYGEN_RUN
class QxtHmacPrivate : public QxtPrivate<QxtHmac>
{
public:
QXT_DECLARE_PUBLIC(QxtHmac)
QxtHmacPrivate() : ohash(0), ihash(0) {}
~QxtHmacPrivate()
{
// deleting NULL is safe, so no tests are needed here
delete ohash;
delete ihash;
}
QCryptographicHash* ohash;
QCryptographicHash* ihash;
QByteArray opad, ipad, result;
QCryptographicHash::Algorithm algorithm;
};
#endif
/*!
* Constructs a QxtHmac object using the specified algorithm.
*/
QxtHmac::QxtHmac(QCryptographicHash::Algorithm algorithm)
{
QXT_INIT_PRIVATE(QxtHmac);
qxt_d().ohash = new QCryptographicHash(algorithm);
qxt_d().ihash = new QCryptographicHash(algorithm);
qxt_d().algorithm = algorithm;
}
/*!
* Sets the shared secret key for the message authentication code.
*
* Any data that had been processed using addData() will be discarded.
*/
void QxtHmac::setKey(QByteArray key)
{
// We make the assumption that all hashes use a 512-bit block size; as of Qt 4.4.0 this is true of all supported hash functions
QxtHmacPrivate* d = &qxt_d();
d->opad = QByteArray(64, 0x5c);
d->ipad = QByteArray(64, 0x36);
if (key.size() > 64)
{
key = QCryptographicHash::hash(key, d->algorithm);
}
for (int i = key.size() - 1; i >= 0; --i)
{
d->opad[i] = d->opad[i] ^ key[i];
d->ipad[i] = d->ipad[i] ^ key[i];
}
reset();
}
/*!
* Resets the object.
*
* Any data that had been processed using addData() will be discarded.
* The key, if set, will be preserved.
*/
void QxtHmac::reset()
{
QxtHmacPrivate* d = &qxt_d();
d->ihash->reset();
d->ihash->addData(d->ipad);
}
/*!
* Returns the inner hash of the HMAC function.
*
* This hash can be stored in lieu of the shared secret on the authenticating side
* and used for verifying an HMAC code. When used in this manner, HMAC can be used
* to provide a form of secure password authentication. See the documentation above
* for details.
*/
QByteArray QxtHmac::innerHash() const
{
return qxt_d().ihash->result();
}
/*!
* Returns the authentication code for the message.
*/
QByteArray QxtHmac::result()
{
QxtHmacPrivate* d = &qxt_d();
Q_ASSERT(d->opad.size());
if (d->result.size())
return d->result;
d->ohash->reset();
d->ohash->addData(d->opad);
d->ohash->addData(innerHash());
d->result = d->ohash->result();
return d->result;
}
/*!
* Verifies the authentication code against a known inner hash.
*
* \sa innerHash()
*/
bool QxtHmac::verify(const QByteArray& otherInner)
{
result(); // populates d->result
QxtHmacPrivate* d = &qxt_d();
d->ohash->reset();
d->ohash->addData(d->opad);
d->ohash->addData(otherInner);
return d->result == d->ohash->result();
}
/*!
* Adds the provided data to the message to be authenticated.
*/
void QxtHmac::addData(const char* data, int length)
{
Q_ASSERT(qxt_d().opad.size());
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
qxt_d().ihash->addData(QByteArrayView(data, length));
#else
qxt_d().ihash->addData(data, length);
#endif
qxt_d().result.clear();
}
/*!
* Adds the provided data to the message to be authenticated.
*/
void QxtHmac::addData(const QByteArray& data)
{
addData(data.constData(), data.size());
}
/*!
* Returns the HMAC of the provided data using the specified key and hashing algorithm.
*/
QByteArray QxtHmac::hash(const QByteArray& key, const QByteArray& data, Algorithm algorithm)
{
QxtHmac hmac(algorithm);
hmac.setKey(key);
hmac.addData(data);
return hmac.result();
}
/*!
* Verifies a HMAC against a known key and inner hash using the specified hashing algorithm.
*/
bool QxtHmac::verify(const QByteArray& key, const QByteArray& hmac, const QByteArray& inner, Algorithm algorithm)
{
QxtHmac calc(algorithm);
calc.setKey(key);
QxtHmacPrivate* d = &calc.qxt_d();
d->ohash->reset();
d->ohash->addData(d->opad);
d->ohash->addData(inner);
return hmac == d->ohash->result();
}
+58
View File
@@ -0,0 +1,58 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtCore module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTHMAC_H
#define QXTHMAC_H
#include <QtGlobal>
#include <QCryptographicHash>
#include "qxtglobal.h"
class QxtHmacPrivate;
class QXT_CORE_EXPORT QxtHmac
{
public:
typedef QCryptographicHash::Algorithm Algorithm;
QxtHmac(QCryptographicHash::Algorithm algorithm);
void setKey(QByteArray key);
void reset();
void addData(const char* data, int length);
void addData(const QByteArray& data);
QByteArray innerHash() const;
QByteArray result();
bool verify(const QByteArray& otherInner);
static QByteArray hash(const QByteArray& key, const QByteArray& data, Algorithm algorithm);
static bool verify(const QByteArray& key, const QByteArray& hmac, const QByteArray& inner, Algorithm algorithm);
private:
QXT_DECLARE_PRIVATE(QxtHmac)
};
#endif
+35
View File
@@ -0,0 +1,35 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTMAIL_P_H
#define QXTMAIL_P_H
#include <QByteArray>
#define QXT_MUST_QP(x) (x < char(32) || x > char(126) || x == '=' || x == '?')
QByteArray qxt_fold_mime_header(const QString &key,
const QString &value,
const QByteArray &prefix = QByteArray());
#endif // QXTMAIL_P_H
@@ -0,0 +1,209 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
/*!
* \class QxtMailAttachment
* \inmodule QxtNetwork
* \brief The QxtMailAttachment class represents an attachement to a QxtMailMessage
*/
#include "qxtmailattachment.h"
#include "qxtmail_p.h"
#include <QBuffer>
#include <QPointer>
#include <QFile>
#include <QtDebug>
struct QxtMailAttachmentPrivate : public QSharedData
{
QHash<QString, QString> extraHeaders;
QString contentType;
QPointer<QIODevice> content;
bool deleteContent;
QxtMailAttachmentPrivate()
{
content = 0;
deleteContent = false;
contentType = "text/plain";
}
~QxtMailAttachmentPrivate()
{
if (deleteContent && content)
content->deleteLater();
deleteContent = false;
content = 0;
}
};
QxtMailAttachment::QxtMailAttachment()
{
qxt_d = new QxtMailAttachmentPrivate;
}
QxtMailAttachment::QxtMailAttachment(const QxtMailAttachment& other) : qxt_d(other.qxt_d)
{
// trivial copy constructor
}
QxtMailAttachment::QxtMailAttachment(const QByteArray& content, const QString& contentType)
{
qxt_d = new QxtMailAttachmentPrivate;
setContentType(contentType);
setContent(content);
}
QxtMailAttachment::QxtMailAttachment(QIODevice* content, const QString& contentType)
{
qxt_d = new QxtMailAttachmentPrivate;
setContentType(contentType);
setContent(content);
}
QxtMailAttachment& QxtMailAttachment::operator=(const QxtMailAttachment & other)
{
qxt_d = other.qxt_d;
return *this;
}
QxtMailAttachment::~QxtMailAttachment()
{
// trivial destructor
}
QIODevice* QxtMailAttachment::content() const
{
return qxt_d->content;
}
void QxtMailAttachment::setContent(const QByteArray& content)
{
if (qxt_d->deleteContent && qxt_d->content)
qxt_d->content->deleteLater();
qxt_d->content = new QBuffer;
static_cast<QBuffer*>(qxt_d->content.data())->setData(content);
}
void QxtMailAttachment::setContent(QIODevice* content)
{
if (qxt_d->deleteContent && qxt_d->content)
qxt_d->content->deleteLater();
qxt_d->content = content;
}
bool QxtMailAttachment::deleteContent() const
{
return qxt_d->deleteContent;
}
void QxtMailAttachment::setDeleteContent(bool enable)
{
qxt_d->deleteContent = enable;
}
QString QxtMailAttachment::contentType() const
{
return qxt_d->contentType;
}
void QxtMailAttachment::setContentType(const QString& contentType)
{
qxt_d->contentType = contentType;
}
QHash<QString, QString> QxtMailAttachment::extraHeaders() const
{
return qxt_d->extraHeaders;
}
QByteArray QxtMailAttachment::extraHeader(const QString& key) const
{
return qxt_d->extraHeaders[key.toLower()].toLatin1();
}
bool QxtMailAttachment::hasExtraHeader(const QString& key) const
{
return qxt_d->extraHeaders.contains(key.toLower());
}
void QxtMailAttachment::setExtraHeader(const QString& key, const QString& value)
{
qxt_d->extraHeaders[key.toLower()] = value;
}
void QxtMailAttachment::setExtraHeaders(const QHash<QString, QString>& a)
{
QHash<QString, QString>& headers = qxt_d->extraHeaders;
headers.clear();
for (const QString& key: a.keys())
{
headers[key.toLower()] = a[key];
}
}
void QxtMailAttachment::removeExtraHeader(const QString& key)
{
qxt_d->extraHeaders.remove(key.toLower());
}
QByteArray QxtMailAttachment::mimeData()
{
QIODevice* c = content();
if (!c)
{
qWarning() << "QxtMailAttachment::mimeData(): Content not set or already output";
return QByteArray();
}
if (!c->isOpen() && !c->open(QIODevice::ReadOnly))
{
qWarning() << "QxtMailAttachment::mimeData(): Cannot open content for reading";
return QByteArray();
}
QByteArray rv = "Content-Type: " + qxt_d->contentType.toLatin1() + "\r\nContent-Transfer-Encoding: base64\r\n";
for(const QString& r: qxt_d->extraHeaders.keys())
{
rv += qxt_fold_mime_header(r.toLatin1(), extraHeader(r));
}
rv += "\r\n";
while (!c->atEnd())
{
rv += c->read(57).toBase64() + "\r\n";
}
setContent((QIODevice*)0);
return rv;
}
QxtMailAttachment QxtMailAttachment::fromFile(const QString& filename)
{
QxtMailAttachment rv(new QFile(filename));
rv.setDeleteContent(true);
return rv;
}
@@ -0,0 +1,73 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTMAILATTACHMENT_H
#define QXTMAILATTACHMENT_H
#include "qxtglobal.h"
#include <QByteArray>
#include <QHash>
#include <QIODevice>
#include <QMetaType>
#include <QSharedDataPointer>
#include <QStringList>
struct QxtMailAttachmentPrivate;
class QXT_NETWORK_EXPORT QxtMailAttachment
{
public:
QxtMailAttachment();
QxtMailAttachment(const QxtMailAttachment &other);
QxtMailAttachment(const QByteArray &content, const QString &contentType = QString("application/octet-stream"));
QxtMailAttachment(QIODevice *content, const QString &contentType = QString("application/octet-stream"));
QxtMailAttachment &operator=(const QxtMailAttachment &other);
~QxtMailAttachment();
static QxtMailAttachment fromFile(const QString &filename);
QIODevice *content() const;
void setContent(const QByteArray &content);
void setContent(QIODevice *content);
bool deleteContent() const;
void setDeleteContent(bool enable);
QString contentType() const;
void setContentType(const QString &contentType);
QHash<QString, QString> extraHeaders() const;
QByteArray extraHeader(const QString &) const;
bool hasExtraHeader(const QString &) const;
void setExtraHeader(const QString &key, const QString &value);
void setExtraHeaders(const QHash<QString, QString> &);
void removeExtraHeader(const QString &key);
QByteArray mimeData();
private:
QSharedDataPointer<QxtMailAttachmentPrivate> qxt_d;
};
Q_DECLARE_TYPEINFO(QxtMailAttachment, Q_MOVABLE_TYPE);
#endif // QXTMAILATTACHMENT_H
+475
View File
@@ -0,0 +1,475 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
/*!
* \class QxtMailMessage
* \inmodule QxtNetwork
* \brief The QxtMailMessage class encapsulates an e-mail according to RFC 2822 and related specifications
*/
//! \todo {implicitshared}
#include "qxtmailmessage.h"
#include "qxtmail_p.h"
#include <QDir>
#include <QUuid>
#include <QtDebug>
static bool isASCII(const QString &string) {
for(const QChar &chr : string){
if(chr.unicode() > 0x7f)
return false;
}
return true;
}
struct QxtMailMessagePrivate : public QSharedData
{
QxtMailMessagePrivate()
{
}
QxtMailMessagePrivate(const QxtMailMessagePrivate &other)
: QSharedData(other), rcptTo(other.rcptTo), rcptCc(other.rcptCc), rcptBcc(other.rcptBcc),
subject(other.subject), body(other.body), sender(other.sender), extraHeaders(other.extraHeaders),
attachments(other.attachments)
{
}
QStringList rcptTo, rcptCc, rcptBcc;
QString subject, body, sender;
QHash<QString, QString> extraHeaders;
QHash<QString, QxtMailAttachment> attachments;
mutable QByteArray boundary;
};
QxtMailMessage::QxtMailMessage()
{
qxt_d = new QxtMailMessagePrivate;
}
QxtMailMessage::QxtMailMessage(const QxtMailMessage &other) : qxt_d(other.qxt_d)
{
// trivial copy constructor
}
QxtMailMessage::QxtMailMessage(const QString &sender, const QString &recipient)
{
qxt_d = new QxtMailMessagePrivate;
setSender(sender);
addRecipient(recipient);
}
QxtMailMessage::~QxtMailMessage()
{
// trivial destructor
}
QxtMailMessage &QxtMailMessage::operator=(const QxtMailMessage &other)
{
qxt_d = other.qxt_d;
return *this;
}
QString QxtMailMessage::sender() const
{
return qxt_d->sender;
}
void QxtMailMessage::setSender(const QString &a)
{
qxt_d->sender = a;
}
QString QxtMailMessage::subject() const
{
return qxt_d->subject;
}
void QxtMailMessage::setSubject(const QString &a)
{
qxt_d->subject = a;
}
QString QxtMailMessage::body() const
{
return qxt_d->body;
}
void QxtMailMessage::setBody(const QString &a)
{
qxt_d->body = a;
}
QStringList QxtMailMessage::recipients(QxtMailMessage::RecipientType type) const
{
if (type == Bcc)
return qxt_d->rcptBcc;
if (type == Cc)
return qxt_d->rcptCc;
return qxt_d->rcptTo;
}
void QxtMailMessage::addRecipient(const QString &a, QxtMailMessage::RecipientType type)
{
if (type == Bcc)
qxt_d->rcptBcc.append(a);
else if (type == Cc)
qxt_d->rcptCc.append(a);
else
qxt_d->rcptTo.append(a);
}
void QxtMailMessage::removeRecipient(const QString &a)
{
qxt_d->rcptTo.removeAll(a);
qxt_d->rcptCc.removeAll(a);
qxt_d->rcptBcc.removeAll(a);
}
QHash<QString, QString> QxtMailMessage::extraHeaders() const
{
return qxt_d->extraHeaders;
}
QByteArray QxtMailMessage::extraHeader(const QString &key) const
{
return qxt_d->extraHeaders[key.toLower()].toLatin1();
}
bool QxtMailMessage::hasExtraHeader(const QString &key) const
{
return qxt_d->extraHeaders.contains(key.toLower());
}
void QxtMailMessage::setExtraHeader(const QString &key, const QString &value)
{
qxt_d->extraHeaders[key.toLower()] = value;
}
void QxtMailMessage::setExtraHeaders(const QHash<QString, QString> &a)
{
QHash<QString, QString> &headers = qxt_d->extraHeaders;
headers.clear();
for (const QString &key : a.keys()) {
headers[key.toLower()] = a[key];
}
}
void QxtMailMessage::removeExtraHeader(const QString &key)
{
qxt_d->extraHeaders.remove(key.toLower());
}
QHash<QString, QxtMailAttachment> QxtMailMessage::attachments() const
{
return qxt_d->attachments;
}
QxtMailAttachment QxtMailMessage::attachment(const QString &filename) const
{
return qxt_d->attachments[filename];
}
void QxtMailMessage::addAttachment(const QString &filename, const QxtMailAttachment &attach)
{
if (qxt_d->attachments.contains(filename)) {
qWarning() << "QxtMailMessage::addAttachment: " << filename << " already in use";
int i = 1;
while (qxt_d->attachments.contains(filename + "." + QString::number(i))) {
i++;
}
qxt_d->attachments[filename + "." + QString::number(i)] = attach;
} else {
qxt_d->attachments[filename] = attach;
}
}
void QxtMailMessage::removeAttachment(const QString &filename)
{
qxt_d->attachments.remove(filename);
}
QByteArray qxt_fold_mime_header(const QString &key, const QString &value, const QByteArray &prefix)
{
QByteArray rv = "";
QByteArray line = key.toLatin1() + ": ";
if (!prefix.isEmpty())
line += prefix;
if (!value.contains("=?") && isASCII(value)) {
bool firstWord = true;
for (const QByteArray &word : value.toLatin1().split(' ')) {
if (line.size() > 78) {
rv = rv + line + "\r\n";
line.clear();
}
if (firstWord)
line += word;
else
line += " " + word;
firstWord = false;
}
} else {
// The text cannot be losslessly encoded as Latin-1. Therefore, we
// must use quoted-printable or base64 encoding. This is a quick
// heuristic based on the first 100 characters to see which
// encoding to use.
QByteArray utf8 = value.toUtf8();
int ct = utf8.length();
int nonAscii = 0;
for (int i = 0; i < ct && i < 100; i++) {
if (QXT_MUST_QP(utf8[i]))
nonAscii++;
}
if (nonAscii > 20) {
// more than 20%-ish non-ASCII characters: use base64
QByteArray base64 = utf8.toBase64();
ct = base64.length();
line += "=?utf-8?b?";
for (int i = 0; i < ct; i += 4) {
if (line.length() > 72) {
rv += line + "?\r\n";
line = " =?utf-8?b?";
}
line = line + base64.mid(i, 4);
}
} else {
// otherwise use Q-encoding
line += "=?utf-8?q?";
for (int i = 0; i < ct; i++) {
if (line.length() > 73) {
rv += line + "?\r\n";
line = " =?utf-8?q?";
}
if (QXT_MUST_QP(utf8[i]) || utf8[i] == ' ') {
line += "=" + utf8.mid(i, 1).toHex().toUpper();
} else {
line += utf8[i];
}
}
}
line += "?="; // end encoded-word atom
}
return rv + line + "\r\n";
}
QByteArray QxtMailMessage::rfc2822() const
{
// Use quoted-printable if requested
bool useQuotedPrintable = (extraHeader("Content-Transfer-Encoding").toLower() == "quoted-printable");
// Use base64 if requested
bool useBase64 = (extraHeader("Content-Transfer-Encoding").toLower() == "base64");
// Check to see if plain text is ASCII-clean; assume it isn't if QP or base64 was requested
bool bodyIsAscii = !useQuotedPrintable && !useBase64 && isASCII(body());
QHash<QString, QxtMailAttachment> attach = attachments();
QByteArray rv;
if (!sender().isEmpty() && !hasExtraHeader("From")) {
rv += qxt_fold_mime_header("From", sender());
}
if (!qxt_d->rcptTo.isEmpty()) {
rv += qxt_fold_mime_header("To", qxt_d->rcptTo.join(", "));
}
if (!qxt_d->rcptCc.isEmpty()) {
rv += qxt_fold_mime_header("Cc", qxt_d->rcptCc.join(", "));
}
if (!subject().isEmpty()) {
rv += qxt_fold_mime_header("Subject", subject());
}
if (!bodyIsAscii) {
if (!hasExtraHeader("MIME-Version") && !attach.count())
rv += "MIME-Version: 1.0\r\n";
// If no transfer encoding has been requested, guess.
// Heuristic: If >20% of the first 100 characters aren't
// 7-bit clean, use base64, otherwise use Q-P.
if (!bodyIsAscii && !useQuotedPrintable && !useBase64) {
QString b = body();
int nonAscii = 0;
int ct = b.length();
for (int i = 0; i < ct && i < 100; i++) {
if (QXT_MUST_QP(b[i]))
nonAscii++;
}
useQuotedPrintable = !(nonAscii > 20);
useBase64 = !useQuotedPrintable;
}
}
if (attach.count()) {
if (qxt_d->boundary.isEmpty())
qxt_d->boundary = QUuid::createUuid().toString().toLatin1().replace("{", "").replace("}", "");
if (!hasExtraHeader("MIME-Version"))
rv += "MIME-Version: 1.0\r\n";
if (!hasExtraHeader("Content-Type"))
rv += "Content-Type: multipart/mixed; boundary=" + qxt_d->boundary + "\r\n";
} else if (!bodyIsAscii && !hasExtraHeader("Content-Transfer-Encoding")) {
if (!useQuotedPrintable) {
// base64
rv += "Content-Transfer-Encoding: base64\r\n";
} else {
// quoted-printable
rv += "Content-Transfer-Encoding: quoted-printable\r\n";
}
}
for (const QString &r : qxt_d->extraHeaders.keys()) {
if ((r.toLower() == "content-type" || r.toLower() == "content-transfer-encoding") && attach.count()) {
// Since we're in multipart mode, we'll be outputting this later
continue;
}
rv += qxt_fold_mime_header(r.toLatin1(), extraHeader(r));
}
rv += "\r\n";
if (attach.count()) {
// we're going to have attachments, so output the lead-in for the message body
rv += "This is a message with multiple parts in MIME format.\r\n";
rv += "--" + qxt_d->boundary + "\r\nContent-Type: ";
if (hasExtraHeader("Content-Type"))
rv += extraHeader("Content-Type") + "\r\n";
else
rv += "text/plain; charset=UTF-8\r\n";
if (hasExtraHeader("Content-Transfer-Encoding")) {
rv += "Content-Transfer-Encoding: " + extraHeader("Content-Transfer-Encoding") + "\r\n";
} else if (!bodyIsAscii) {
if (!useQuotedPrintable) {
// base64
rv += "Content-Transfer-Encoding: base64\r\n";
} else {
// quoted-printable
rv += "Content-Transfer-Encoding: quoted-printable\r\n";
}
}
rv += "\r\n";
}
if (bodyIsAscii) {
QByteArray b = body().toLatin1();
int len = b.length();
QByteArray line = "";
QByteArray word = "";
for (int i = 0; i < len; i++) {
if (b[i] == '\n' || b[i] == '\r') {
if (line.isEmpty()) {
line = word;
word = "";
} else if (line.length() + word.length() + 1 <= 78) {
line = line + ' ' + word;
word = "";
}
if (line.isEmpty())
continue;
if (line[0] == '.')
rv += ".";
rv += line + "\r\n";
if ((b[i + 1] == '\n' || b[i + 1] == '\r') && b[i] != b[i + 1]) {
// If we're looking at a CRLF pair, skip the second half
i++;
}
line = word;
} else if (b[i] == ' ') {
if (line.length() + word.length() + 1 > 78) {
if (line[0] == '.')
rv += ".";
rv += line + "\r\n";
line = word;
} else if (line.isEmpty()) {
line = word;
} else {
line = line + ' ' + word;
}
word = "";
} else {
word += b[i];
}
}
if (line.length() + word.length() + 1 > 78) {
if (line[0] == '.')
rv += ".";
rv += line + "\r\n";
line = word;
} else if (!word.isEmpty()) {
line += ' ' + word;
}
if (!line.isEmpty()) {
if (line[0] == '.')
rv += ".";
rv += line + "\r\n";
}
} else if (useQuotedPrintable) {
QByteArray b = body().toUtf8();
int ct = b.length();
QByteArray line;
for (int i = 0; i < ct; i++) {
if (b[i] == '\n' || b[i] == '\r') {
if (line[0] == '.')
rv += ".";
rv += line + "\r\n";
line = "";
if ((b[i + 1] == '\n' || b[i + 1] == '\r') && b[i] != b[i + 1]) {
// If we're looking at a CRLF pair, skip the second half
i++;
}
} else if (line.length() > 74) {
rv += line + "=\r\n";
line = "";
}
if (QXT_MUST_QP(b[i])) {
line += "=" + b.mid(i, 1).toHex().toUpper();
} else {
line += b[i];
}
}
if (!line.isEmpty()) {
if (line[0] == '.')
rv += ".";
rv += line + "\r\n";
}
} else /* base64 */
{
QByteArray b = body().toUtf8().toBase64();
int ct = b.length();
for (int i = 0; i < ct; i += 78) {
rv += b.mid(i, 78) + "\r\n";
}
}
if (attach.count()) {
for (const QString &filename : attach.keys()) {
rv += "--" + qxt_d->boundary + "\r\n";
rv +=
qxt_fold_mime_header("Content-Disposition", QDir(filename).dirName(), "attachment; filename=");
rv += attach[filename].mimeData();
}
rv += "--" + qxt_d->boundary + "--\r\n";
}
return rv;
}
+85
View File
@@ -0,0 +1,85 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTMAILMESSAGE_H
#define QXTMAILMESSAGE_H
#include "qxtglobal.h"
#include "qxtmailattachment.h"
#include <QStringList>
#include <QHash>
#include <QMetaType>
#include <QSharedDataPointer>
struct QxtMailMessagePrivate;
class QXT_NETWORK_EXPORT QxtMailMessage
{
public:
enum RecipientType
{
To,
Cc,
Bcc
};
QxtMailMessage();
QxtMailMessage(const QxtMailMessage& other);
QxtMailMessage(const QString& sender, const QString& recipient);
QxtMailMessage& operator=(const QxtMailMessage& other);
~QxtMailMessage();
QString sender() const;
void setSender(const QString&);
QString subject() const;
void setSubject(const QString&);
QString body() const;
void setBody(const QString&);
QStringList recipients(RecipientType type = To) const;
void addRecipient(const QString&, RecipientType type = To);
void removeRecipient(const QString&);
QHash<QString, QString> extraHeaders() const;
QByteArray extraHeader(const QString&) const;
bool hasExtraHeader(const QString&) const;
void setExtraHeader(const QString& key, const QString& value);
void setExtraHeaders(const QHash<QString, QString>&);
void removeExtraHeader(const QString& key);
QHash<QString, QxtMailAttachment> attachments() const;
QxtMailAttachment attachment(const QString& filename) const;
void addAttachment(const QString& filename, const QxtMailAttachment& attach);
void removeAttachment(const QString& filename);
QByteArray rfc2822() const;
private:
QSharedDataPointer<QxtMailMessagePrivate> qxt_d;
};
Q_DECLARE_TYPEINFO(QxtMailMessage, Q_MOVABLE_TYPE);
#endif // QXTMAIL_H
+536
View File
@@ -0,0 +1,536 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
/*!
* \class QxtSmtp
* \inmodule QxtNetwork
* \brief The QxtSmtp class implements the SMTP protocol for sending email
*/
#include "qxtsmtp.h"
#include "qxthmac.h"
#include "qxtsmtp_p.h"
#include <QNetworkInterface>
#include <QSslSocket>
#include <QStringList>
#include <QTcpSocket>
QxtSmtpPrivate::QxtSmtpPrivate() : QObject(0)
{
// empty ctor
}
QxtSmtp::QxtSmtp(QObject *parent) : QObject(parent)
{
QXT_INIT_PRIVATE(QxtSmtp);
qxt_d().state = QxtSmtpPrivate::Disconnected;
qxt_d().nextID = 0;
qxt_d().socket = new QSslSocket(this);
QObject::connect(socket(), SIGNAL(encrypted()), this, SIGNAL(encrypted()));
// QObject::connect(socket(), SIGNAL(encrypted()), &qxt_d(), SLOT(ehlo()));
QObject::connect(socket(), SIGNAL(connected()), this, SIGNAL(connected()));
QObject::connect(socket(), SIGNAL(disconnected()), this, SIGNAL(disconnected()));
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
QObject::connect(socket(), SIGNAL(errorOccurred(QAbstractSocket::SocketError)), &qxt_d(),
SLOT(socketError(QAbstractSocket::SocketError)));
#else
QObject::connect(socket(), SIGNAL(error(QAbstractSocket::SocketError)), &qxt_d(),
SLOT(socketError(QAbstractSocket::SocketError)));
#endif
QObject::connect(this, SIGNAL(authenticated()), &qxt_d(), SLOT(sendNext()));
QObject::connect(socket(), SIGNAL(readyRead()), &qxt_d(), SLOT(socketRead()));
}
QByteArray QxtSmtp::username() const
{
return qxt_d().username;
}
void QxtSmtp::setUsername(const QByteArray &username)
{
qxt_d().username = username;
}
QByteArray QxtSmtp::password() const
{
return qxt_d().password;
}
void QxtSmtp::setPassword(const QByteArray &password)
{
qxt_d().password = password;
}
int QxtSmtp::send(const QxtMailMessage &message)
{
int messageID = ++qxt_d().nextID;
qxt_d().pending.append(qMakePair(messageID, message));
if (qxt_d().state == QxtSmtpPrivate::Waiting)
qxt_d().sendNext();
return messageID;
}
int QxtSmtp::pendingMessages() const
{
return qxt_d().pending.count();
}
QTcpSocket *QxtSmtp::socket() const
{
return qxt_d().socket;
}
void QxtSmtp::connectToHost(const QString &hostName, quint16 port)
{
qxt_d().useSecure = false;
qxt_d().state = QxtSmtpPrivate::StartState;
socket()->connectToHost(hostName, port);
}
void QxtSmtp::connectToHost(const QHostAddress &address, quint16 port)
{
connectToHost(address.toString(), port);
}
void QxtSmtp::disconnectFromHost()
{
socket()->disconnectFromHost();
}
bool QxtSmtp::startTlsDisabled() const
{
return qxt_d().disableStartTLS;
}
void QxtSmtp::setStartTlsDisabled(bool disable)
{
qxt_d().disableStartTLS = disable;
}
QSslSocket *QxtSmtp::sslSocket() const
{
return qxt_d().socket;
}
void QxtSmtp::connectToSecureHost(const QString &hostName, quint16 port)
{
qxt_d().useSecure = true;
qxt_d().state = QxtSmtpPrivate::StartState;
sslSocket()->connectToHostEncrypted(hostName, port);
}
void QxtSmtp::connectToSecureHost(const QHostAddress &address, quint16 port)
{
connectToSecureHost(address.toString(), port);
}
bool QxtSmtp::hasExtension(const QString &extension)
{
return qxt_d().extensions.contains(extension);
}
QString QxtSmtp::extensionData(const QString &extension)
{
return qxt_d().extensions[extension];
}
void QxtSmtpPrivate::socketError(QAbstractSocket::SocketError err)
{
if (err == QAbstractSocket::SslHandshakeFailedError) {
emit qxt_p().encryptionFailed();
emit qxt_p().encryptionFailed(socket->errorString().toLatin1());
} else if (state == StartState) {
emit qxt_p().connectionFailed();
emit qxt_p().connectionFailed(socket->errorString().toLatin1());
}
}
void QxtSmtpPrivate::socketRead()
{
buffer += socket->readAll();
while (true) {
int pos = buffer.indexOf("\r\n");
if (pos < 0)
return;
QByteArray line = buffer.left(pos);
buffer = buffer.mid(pos + 2);
QByteArray code = line.left(3);
switch (state) {
case StartState:
if (code[0] != '2') {
socket->disconnectFromHost();
} else {
ehlo();
}
break;
case HeloSent:
case EhloSent:
case EhloGreetReceived:
parseEhlo(code, (line[3] != ' '), line.mid(4));
break;
case StartTLSSent:
if (code == "220") {
socket->startClientEncryption();
ehlo();
} else {
authenticate();
}
break;
case AuthRequestSent:
case AuthUsernameSent:
if (authType == AuthPlain)
authPlain();
else if (authType == AuthLogin)
authLogin();
else
authCramMD5(line.mid(4));
break;
case AuthSent:
if (code[0] == '2') {
state = Authenticated;
emit qxt_p().authenticated();
} else {
state = Disconnected;
emit qxt_p().authenticationFailed();
emit qxt_p().authenticationFailed(line);
emit socket->disconnectFromHost();
}
break;
case MailToSent:
case RcptAckPending:
if (code[0] != '2') {
emit qxt_p().mailFailed(pending.first().first, code.toInt());
emit qxt_p().mailFailed(pending.first().first, code.toInt(), line);
// pending.removeFirst();
// DO NOT remove it, the body sent state needs this message to assigned the next mail failed message
// that will the sendNext a reset will be sent to clear things out
sendNext();
state = BodySent;
} else
sendNextRcpt(code, line);
break;
case SendingBody:
sendBody(code, line);
break;
case BodySent:
if (pending.count()) {
// if you removeFirst in RcpActpending/MailToSent on an error, and the queue is now empty,
// you will get into this state and then crash because no check is done. CHeck added but shouldnt
// be necessary since I commented out the removeFirst
if (code[0] != '2') {
emit qxt_p().mailFailed(pending.first().first, code.toInt());
emit qxt_p().mailFailed(pending.first().first, code.toInt(), line);
} else
emit qxt_p().mailSent(pending.first().first);
pending.removeFirst();
}
sendNext();
break;
case Resetting:
if (code[0] != '2') {
emit qxt_p().connectionFailed();
emit qxt_p().connectionFailed(line);
} else {
state = Waiting;
sendNext();
}
break;
case Disconnected:
case EhloExtensionsReceived:
case EhloDone:
case Authenticated:
case Waiting:
// only to make compiler happy
break;
}
}
}
void QxtSmtpPrivate::ehlo()
{
QByteArray address = "127.0.0.1";
for (const QHostAddress &addr : QNetworkInterface::allAddresses()) {
if (addr == QHostAddress::LocalHost || addr == QHostAddress::LocalHostIPv6)
continue;
address = addr.toString().toLatin1();
break;
}
socket->write("ehlo " + address + "\r\n");
extensions.clear();
state = EhloSent;
}
void QxtSmtpPrivate::parseEhlo(const QByteArray &code, bool cont, const QString &line)
{
if (code != "250") {
// error!
if (state != HeloSent) {
// maybe let's try HELO
socket->write("helo\r\n");
state = HeloSent;
} else {
// nope
socket->write("QUIT\r\n");
socket->flush();
socket->disconnectFromHost();
}
return;
} else if (state != EhloGreetReceived) {
if (!cont) {
// greeting only, no extensions
state = EhloDone;
} else {
// greeting followed by extensions
state = EhloGreetReceived;
return;
}
} else {
extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1);
if (!cont)
state = EhloDone;
}
if (state != EhloDone)
return;
if (extensions.contains("STARTTLS") && !disableStartTLS) {
startTLS();
} else {
authenticate();
}
}
void QxtSmtpPrivate::startTLS()
{
socket->write("starttls\r\n");
state = StartTLSSent;
}
void QxtSmtpPrivate::authenticate()
{
if (!extensions.contains("AUTH") || username.isEmpty() || password.isEmpty()) {
state = Authenticated;
emit qxt_p().authenticated();
} else {
QStringList auth = extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts);
if (auth.contains("CRAM-MD5")) {
authCramMD5();
} else if (auth.contains("PLAIN")) {
authPlain();
} else if (auth.contains("LOGIN")) {
authLogin();
} else {
state = Authenticated;
emit qxt_p().authenticated();
}
}
}
void QxtSmtpPrivate::authCramMD5(const QByteArray &challenge)
{
if (state != AuthRequestSent) {
socket->write("auth cram-md5\r\n");
authType = AuthCramMD5;
state = AuthRequestSent;
} else {
QxtHmac hmac(QCryptographicHash::Md5);
hmac.setKey(password);
hmac.addData(QByteArray::fromBase64(challenge));
QByteArray response = username + ' ' + hmac.result().toHex();
socket->write(response.toBase64() + "\r\n");
state = AuthSent;
}
}
void QxtSmtpPrivate::authPlain()
{
if (state != AuthRequestSent) {
socket->write("auth plain\r\n");
authType = AuthPlain;
state = AuthRequestSent;
} else {
QByteArray auth;
auth += '\0';
auth += username;
auth += '\0';
auth += password;
socket->write(auth.toBase64() + "\r\n");
state = AuthSent;
}
}
void QxtSmtpPrivate::authLogin()
{
if (state != AuthRequestSent && state != AuthUsernameSent) {
socket->write("auth login\r\n");
authType = AuthLogin;
state = AuthRequestSent;
} else if (state == AuthRequestSent) {
socket->write(username.toBase64() + "\r\n");
state = AuthUsernameSent;
} else {
socket->write(password.toBase64() + "\r\n");
state = AuthSent;
}
}
static QByteArray qxt_extract_address(const QString &address)
{
int parenDepth = 0;
int addrStart = -1;
bool inQuote = false;
int ct = address.length();
for (int i = 0; i < ct; i++) {
QChar ch = address[i];
if (inQuote) {
if (ch == '"')
inQuote = false;
} else if (addrStart != -1) {
if (ch == '>')
return address.mid(addrStart, (i - addrStart)).toLatin1();
} else if (ch == '(') {
parenDepth++;
} else if (ch == ')') {
parenDepth--;
if (parenDepth < 0)
parenDepth = 0;
} else if (ch == '"') {
if (parenDepth == 0)
inQuote = true;
} else if (ch == '<') {
if (!inQuote && parenDepth == 0)
addrStart = i + 1;
}
}
return address.toLatin1();
}
void QxtSmtpPrivate::sendNext()
{
if (state == Disconnected) {
// leave the mail in the queue if not ready to send
return;
}
if (pending.isEmpty()) {
// if there are no additional mails to send, finish up
state = Waiting;
emit qxt_p().finished();
return;
}
if (state != Waiting) {
state = Resetting;
socket->write("rset\r\n");
return;
}
const QxtMailMessage &msg = pending.first().second;
rcptNumber = rcptAck = mailAck = 0;
recipients =
msg.recipients(QxtMailMessage::To) + msg.recipients(QxtMailMessage::Cc) + msg.recipients(QxtMailMessage::Bcc);
if (recipients.count() == 0) {
// can't send an e-mail with no recipients
emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients);
emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients, QByteArray("e-mail has no recipients"));
pending.removeFirst();
sendNext();
return;
}
// We explicitly use lowercase keywords because for some reason gmail
// interprets any string starting with an uppercase R as a request
// to renegotiate the SSL connection.
socket->write("mail from:<" + qxt_extract_address(msg.sender()) + ">\r\n");
if (extensions.contains("PIPELINING")) // almost all do nowadays
{
for (const QString &rcpt : recipients) {
socket->write("rcpt to:<" + qxt_extract_address(rcpt) + ">\r\n");
}
state = RcptAckPending;
} else {
state = MailToSent;
}
}
void QxtSmtpPrivate::sendNextRcpt(const QByteArray &code, const QByteArray &line)
{
int messageID = pending.first().first;
const QxtMailMessage &msg = pending.first().second;
if (code[0] != '2') {
// on failure, emit a warning signal
if (!mailAck) {
emit qxt_p().senderRejected(messageID, msg.sender());
emit qxt_p().senderRejected(messageID, msg.sender(), line);
} else {
emit qxt_p().recipientRejected(messageID, msg.sender());
emit qxt_p().recipientRejected(messageID, msg.sender(), line);
}
} else if (!mailAck) {
mailAck = true;
} else {
rcptAck++;
}
if (rcptNumber == recipients.count()) {
// all recipients have been sent
if (rcptAck == 0) {
// no recipients were considered valid
emit qxt_p().mailFailed(messageID, code.toInt());
emit qxt_p().mailFailed(messageID, code.toInt(), line);
pending.removeFirst();
sendNext();
} else {
// at least one recipient was acknowledged, send mail body
socket->write("data\r\n");
state = SendingBody;
}
} else if (state != RcptAckPending) {
// send the next recipient unless we're only waiting on acks
socket->write("rcpt to:<" + qxt_extract_address(recipients[rcptNumber]) + ">\r\n");
rcptNumber++;
} else {
// If we're only waiting on acks, just count them
rcptNumber++;
}
}
void QxtSmtpPrivate::sendBody(const QByteArray &code, const QByteArray &line)
{
int messageID = pending.first().first;
const QxtMailMessage &msg = pending.first().second;
if (code[0] != '3') {
emit qxt_p().mailFailed(messageID, code.toInt());
emit qxt_p().mailFailed(messageID, code.toInt(), line);
pending.removeFirst();
sendNext();
return;
}
socket->write(msg.rfc2822());
socket->write(".\r\n");
state = BodySent;
}
+111
View File
@@ -0,0 +1,111 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTSMTP_H
#define QXTSMTP_H
#include <QObject>
#include <QHostAddress>
#include <QString>
#include "qxtglobal.h"
#include "qxtmailmessage.h"
class QTcpSocket;
class QSslSocket;
class QxtSmtpPrivate;
class QXT_NETWORK_EXPORT QxtSmtp : public QObject
{
Q_OBJECT
public:
enum SmtpError
{
NoError,
NoRecipients,
CommandUnrecognized = 500,
SyntaxError,
CommandNotImplemented,
BadSequence,
ParameterNotImplemented,
MailboxUnavailable = 550,
UserNotLocal,
MessageTooLarge,
InvalidMailboxName,
TransactionFailed
};
QxtSmtp(QObject* parent = 0);
QByteArray username() const;
void setUsername(const QByteArray& name);
QByteArray password() const;
void setPassword(const QByteArray& password);
int send(const QxtMailMessage& message);
int pendingMessages() const;
QTcpSocket* socket() const;
void connectToHost(const QString& hostName, quint16 port = 25);
void connectToHost(const QHostAddress& address, quint16 port = 25);
void disconnectFromHost();
bool startTlsDisabled() const;
void setStartTlsDisabled(bool disable);
QSslSocket* sslSocket() const;
void connectToSecureHost(const QString& hostName, quint16 port = 465);
void connectToSecureHost(const QHostAddress& address, quint16 port = 465);
bool hasExtension(const QString& extension);
QString extensionData(const QString& extension);
Q_SIGNALS:
void connected();
void connectionFailed();
void connectionFailed( const QByteArray & msg );
void encrypted();
void encryptionFailed();
void encryptionFailed( const QByteArray & msg );
void authenticated();
void authenticationFailed();
void authenticationFailed( const QByteArray & msg );
void senderRejected(int mailID, const QString& address );
void senderRejected(int mailID, const QString& address, const QByteArray & msg );
void recipientRejected(int mailID, const QString& address );
void recipientRejected(int mailID, const QString& address, const QByteArray & msg );
void mailFailed(int mailID, int errorCode);
void mailFailed(int mailID, int errorCode, const QByteArray & msg);
void mailSent(int mailID);
void finished();
void disconnected();
private:
QXT_DECLARE_PRIVATE(QxtSmtp)
};
#endif // QXTSMTP_H
+102
View File
@@ -0,0 +1,102 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtWeb module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTSMTP_P_H
#define QXTSMTP_P_H
#include "qxtsmtp.h"
#include <QHash>
#include <QString>
#include <QList>
#include <QPair>
class QxtSmtpPrivate : public QObject, public QxtPrivate<QxtSmtp>
{
Q_OBJECT
public:
QxtSmtpPrivate();
QXT_DECLARE_PUBLIC(QxtSmtp)
enum SmtpState
{
Disconnected,
StartState,
EhloSent,
EhloGreetReceived,
EhloExtensionsReceived,
EhloDone,
HeloSent,
StartTLSSent,
AuthRequestSent,
AuthUsernameSent,
AuthSent,
Authenticated,
MailToSent,
RcptAckPending,
SendingBody,
BodySent,
Waiting,
Resetting
};
enum AuthType
{
AuthPlain,
AuthLogin,
AuthCramMD5
};
bool useSecure, disableStartTLS;
SmtpState state;// rather then an int use the enum. makes sure invalid states are entered at compile time, and makes debugging easier
AuthType authType;
QByteArray buffer, username, password;
QHash<QString, QString> extensions;
QList<QPair<int, QxtMailMessage> > pending;
QStringList recipients;
int nextID, rcptNumber, rcptAck;
bool mailAck;
QSslSocket* socket;
void parseEhlo(const QByteArray& code, bool cont, const QString& line);
void startTLS();
void authenticate();
void authCramMD5(const QByteArray& challenge = QByteArray());
void authPlain();
void authLogin();
void sendNextRcpt(const QByteArray& code, const QByteArray & line);
void sendBody(const QByteArray& code, const QByteArray & line);
public slots:
void socketError(QAbstractSocket::SocketError err);
void socketRead();
void ehlo();
void sendNext();
};
#endif // QXTSMTP_P_H
+217
View File
@@ -0,0 +1,217 @@
#include "smtpclient.h"
#include "settingscache.h"
#include "smtp/qxtsmtp.h"
#include <QSslSocket>
#include <QTcpSocket>
SmtpClient::SmtpClient(QObject *parent) : QObject(parent)
{
smtp = new QxtSmtp(this);
connect(smtp, SIGNAL(authenticated()), this, SLOT(authenticated()));
connect(smtp, SIGNAL(authenticationFailed(const QByteArray &)), this,
SLOT(authenticationFailed(const QByteArray &)));
connect(smtp, SIGNAL(connected()), this, SLOT(connected()));
connect(smtp, SIGNAL(connectionFailed(const QByteArray &)), this, SLOT(connectionFailed(const QByteArray &)));
connect(smtp, SIGNAL(disconnected()), this, SLOT(disconnected()));
connect(smtp, SIGNAL(encrypted()), this, SLOT(encrypted()));
connect(smtp, SIGNAL(encryptionFailed(const QByteArray &)), this, SLOT(encryptionFailed(const QByteArray &)));
connect(smtp, SIGNAL(finished()), this, SLOT(finished()));
connect(smtp, SIGNAL(mailFailed(int, int, const QByteArray &)), this,
SLOT(mailFailed(int, int, const QByteArray &)));
connect(smtp, SIGNAL(mailSent(int)), this, SLOT(mailSent(int)));
connect(smtp, SIGNAL(recipientRejected(int, const QString &, const QByteArray &)), this,
SLOT(recipientRejected(int, const QString &, const QByteArray &)));
connect(smtp, SIGNAL(senderRejected(int, const QString &, const QByteArray &)), this,
SLOT(senderRejected(int, const QString &, const QByteArray &)));
}
SmtpClient::~SmtpClient()
{
if (smtp) {
delete smtp;
smtp = 0;
}
}
bool SmtpClient::enqueueActivationTokenMail(const QString &nickname, const QString &recipient, const QString &token)
{
QString email = settingsCache->value("smtp/email", "").toString();
QString name = settingsCache->value("smtp/name", "").toString();
QString subject = settingsCache->value("smtp/subject", "").toString();
QString body = settingsCache->value("smtp/body", "").toString();
if (email.isEmpty()) {
qDebug() << "[MAIL] Missing sender email in configuration";
return false;
}
if (subject.isEmpty()) {
qDebug() << "[MAIL] Missing subject field in configuration";
return false;
}
if (body.isEmpty()) {
qDebug() << "[MAIL] Missing body field in configuration";
return false;
}
if (recipient.isEmpty()) {
qDebug() << "[MAIL] Missing recipient field for user " << nickname;
return false;
}
if (token.isEmpty()) {
qDebug() << "[MAIL] Missing token field for user " << nickname;
return false;
}
QxtMailMessage message;
message.setSender(name + " <" + email + ">");
message.addRecipient(recipient);
message.setSubject(subject);
message.setBody(body.replace("%username", nickname).replace("%token", token));
int id = smtp->send(message);
qDebug() << "[MAIL] Enqueued mail to" << recipient << "as" << id;
return true;
}
bool SmtpClient::enqueueForgotPasswordTokenMail(const QString &nickname, const QString &recipient, const QString &token)
{
QString email = settingsCache->value("smtp/email", "").toString();
QString name = settingsCache->value("smtp/name", "").toString();
QString subject = settingsCache->value("forgotpassword/subject", "").toString();
QString body = settingsCache->value("forgotpassword/body", "").toString();
if (email.isEmpty()) {
qDebug() << "[MAIL] Missing sender email in configuration";
return false;
}
if (subject.isEmpty()) {
qDebug() << "[MAIL] Missing subject field in configuration";
return false;
}
if (body.isEmpty()) {
qDebug() << "[MAIL] Missing body field in configuration";
return false;
}
if (recipient.isEmpty()) {
qDebug() << "[MAIL] Missing recipient field for user " << nickname;
return false;
}
if (token.isEmpty()) {
qDebug() << "[MAIL] Missing token field for user " << nickname;
return false;
}
QxtMailMessage message;
message.setSender(name + " <" + email + ">");
message.addRecipient(recipient);
message.setSubject(subject);
message.setBody(body.replace("%username", nickname).replace("%token", token));
int id = smtp->send(message);
qDebug() << "[MAIL] Enqueued mail to" << recipient << "as" << id;
return true;
}
void SmtpClient::sendAllEmails()
{
// still connected from the previous round
if (smtp->socket()->state() == QAbstractSocket::ConnectedState) {
return;
}
if (smtp->pendingMessages() == 0) {
return;
}
QString connectionType = settingsCache->value("smtp/connection", "tcp").toString();
QString host = settingsCache->value("smtp/host", "localhost").toString();
int port = settingsCache->value("smtp/port", 25).toInt();
QByteArray username = settingsCache->value("smtp/username", "").toByteArray();
QByteArray password = settingsCache->value("smtp/password", "").toByteArray();
bool acceptAllCerts = settingsCache->value("smtp/acceptallcerts", false).toBool();
smtp->setUsername(username);
smtp->setPassword(password);
// Connect
if (connectionType == "ssl") {
if (acceptAllCerts) {
smtp->sslSocket()->setPeerVerifyMode(QSslSocket::QueryPeer);
}
smtp->connectToSecureHost(host, port);
} else {
smtp->connectToHost(host, port);
}
}
void SmtpClient::authenticated()
{
qDebug() << "[MAIL] authenticated";
}
void SmtpClient::authenticationFailed(const QByteArray &msg)
{
qDebug() << "[MAIL] authenticationFailed" << QString(msg);
}
void SmtpClient::connected()
{
qDebug() << "[MAIL] connected";
}
void SmtpClient::connectionFailed(const QByteArray &msg)
{
qDebug() << "[MAIL] connectionFailed" << QString(msg);
}
void SmtpClient::disconnected()
{
qDebug() << "[MAIL] disconnected";
}
void SmtpClient::encrypted()
{
qDebug() << "[MAIL] encrypted";
}
void SmtpClient::encryptionFailed(const QByteArray &msg)
{
qDebug() << "[MAIL] encryptionFailed" << QString(msg);
qDebug() << "[MAIL] Try enabling the \"acceptallcerts\" option in servatrice.ini";
}
void SmtpClient::finished()
{
qDebug() << "[MAIL] finished";
smtp->disconnectFromHost();
}
void SmtpClient::mailFailed(int mailID, int errorCode, const QByteArray &msg)
{
qDebug() << "[MAIL] mailFailed id=" << mailID << " errorCode=" << errorCode << "msg=" << QString(msg);
}
void SmtpClient::mailSent(int mailID)
{
qDebug() << "[MAIL] mailSent" << mailID;
}
void SmtpClient::recipientRejected(int mailID, const QString &address, const QByteArray &msg)
{
qDebug() << "[MAIL] recipientRejected id=" << mailID << " address=" << address << "msg=" << QString(msg);
}
void SmtpClient::senderRejected(int mailID, const QString &address, const QByteArray &msg)
{
qDebug() << "[MAIL] senderRejected id=" << mailID << " address=" << address << "msg=" << QString(msg);
}
+37
View File
@@ -0,0 +1,37 @@
#ifndef SMTPCLIENT_H
#define SMTPCLIENT_H
#include <QObject>
class QxtSmtp;
class QxtMailMessage;
class SmtpClient : public QObject
{
Q_OBJECT
public:
SmtpClient(QObject *parent = 0);
~SmtpClient();
protected:
QxtSmtp *smtp;
public slots:
bool enqueueActivationTokenMail(const QString &nickname, const QString &recipient, const QString &token);
bool enqueueForgotPasswordTokenMail(const QString &nickname, const QString &recipient, const QString &token);
void sendAllEmails();
protected slots:
void authenticated();
void authenticationFailed(const QByteArray &msg);
void connected();
void connectionFailed(const QByteArray &msg);
void disconnected();
void encrypted();
void encryptionFailed(const QByteArray &msg);
void finished();
void mailFailed(int mailID, int errorCode, const QByteArray &msg);
void mailSent(int mailID);
void recipientRejected(int mailID, const QString &address, const QByteArray &msg);
void senderRejected(int mailID, const QString &address, const QByteArray &msg);
};
#endif