Initial commit of mtgonline project
This commit is contained in:
@@ -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()
|
||||
Executable
+36
@@ -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;
|
||||
@@ -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 |
@@ -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
@@ -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);"
|
||||
@@ -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');
|
||||
@@ -0,0 +1,3 @@
|
||||
[client]
|
||||
user={db_username}
|
||||
password={db_password}
|
||||
Executable
+73
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/" >
|
||||
<file alias="resources/appicon.svg">resources/servatrice.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1 @@
|
||||
ID1_ICON1 ICON DISCARDABLE "resources/appicon.ico"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
@@ -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 ®ExpStr : 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user