Изменения для совместимости с сервером прокси апача

This commit is contained in:
Oleg 2025-05-16 05:49:46 +00:00
parent d5d5497dd6
commit 6c87028631
9 changed files with 245 additions and 201 deletions

249
bc.js
View File

@ -1,228 +1,99 @@
// bc.js - Главный файл сервера Battle Club // bc.js - Главный файл сервера Battle Club
const express = require('express'); const express = require('express');
const http = require('http'); const http = require('http'); // Используем HTTP, так как SSL терминируется Apache
const { Server } = require('socket.io'); const { Server } = require('socket.io');
const path = require('path'); const path = require('path');
// Импорт серверных модулей // Импорт ваших серверных модулей (предполагаем, что они есть и работают)
const auth = require('./server_modules/auth'); // const auth = require('./server_modules/auth');
const GameManager = require('./server_modules/gameManager'); // const GameManager = require('./server_modules/gameManager');
const db = require('./server_modules/db'); // Импорт для инициализации соединения с БД (хотя пул создается при require) // const db = require('./server_modules/db');
const GAME_CONFIG = require('./server_modules/config'); // Конфиг игры // const GAME_CONFIG = require('./server_modules/config');
// gameData импортируется внутри GameInstance и GameLogic
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const BC_APP_INTERNAL_PORT = 3200; // Внутренний порт, на котором слушает bc.js
const BC_APP_INTERNAL_HOST = '127.0.0.1'; // Слушать только на localhost
const PUBLIC_PATH_PREFIX = '/battleclub'; // Публичный префикс пути, по которому приложение доступно через Apache
// Настройка Socket.IO // Настройка Socket.IO
// cors options могут потребоваться, если клиент и сервер работают на разных портах/доменах // Клиент будет подключаться к /battleclub/socket.io/
const io = new Server(server, { const io = new Server(server, {
path: `${PUBLIC_PATH_PREFIX}/socket.io`,
cors: { cors: {
origin: "*", // Разрешить подключение с любого домена (для разработки). В продакшене лучше указать конкретный домен клиента. origin: "https://pavel-chagovsky.com", // Укажите ваш домен для безопасности
// origin: "*", // Для разработки можно оставить, но для продакшена лучше конкретный домен
methods: ["GET", "POST"] methods: ["GET", "POST"]
} }
}); });
// Middleware для логирования каждого запроса (полезно для отладки)
app.use((req, res, next) => {
console.log(`[BC App] Request: ${req.method} ${req.originalUrl}`);
next();
});
// Раздача статических файлов из папки 'public' // Раздача статических файлов из папки 'public'
// Так как Apache проксирует /battleclub/ на корень этого Express-приложения,
// Express должен отдавать статику от своего корня.
// В HTML ссылки на статику должны быть относительными или начинаться с /battleclub/
// Например, если в public/js/client.js, то в HTML: <script src="/battleclub/js/client.js"></script>
// Или, если index.html отдается с /battleclub/, то <script src="js/client.js"></script>
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
// Создаем экземпляр GameManager
const gameManager = new GameManager(io);
// Хранилище информации о залогиненных пользователях по socket.id // Пример простого маршрута API, если он нужен (доступен по /battleclub/api/test)
// В более сложном приложении здесь может быть Redis или другое внешнее хранилище сессий app.get('/api/test', (req, res) => {
const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } } res.json({ message: 'Battle Club API is working!' });
});
// Обработка подключений Socket.IO // Если ваше основное приложение - это SPA (Single Page Application),
// вам может понадобиться отдавать index.html для всех не-API и не-статических путей,
// начинающихся с префикса /battleclub/. Но так как Apache проксирует /battleclub/ на /,
// то Express будет видеть пути без /battleclub/.
// Поэтому, если index.html должен отдаваться для /battleclub/ или /battleclub/some/path,
// то здесь нужен роут для '*' или специфичные роуты.
// Пока для простоты, предположим, что Apache проксирует /battleclub/ на корень bc.js,
// и index.html находится в public/ и запрашивается как /battleclub/index.html (или просто /battleclub/)
app.get('/', (req, res) => {
// Этот роут будет срабатывать, если Apache проксировал /battleclub/ на / этого приложения
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// --- Обработчики событий Socket.IO ---
io.on('connection', (socket) => { io.on('connection', (socket) => {
console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`); console.log(`[BC App Socket.IO] User connected: ${socket.id} to path ${socket.nsp.name}`);
// Привязываем user data к сокету (пока пустые) socket.on('messageFromClient', (data) => {
socket.userData = null; // { userId: ..., username: ... } console.log(`[BC App Socket.IO] Message from client ${socket.id}:`, data);
socket.emit('messageFromServer', { text: `Server received: ${data.text}` });
// При подключении клиента, если он уже залогинен (например, по cookie/token, что здесь не реализовано,
// но может быть добавлено), нужно восстановить его user data и проверить, не в игре ли он.
// В текущей простой реализации, мы полагаемся на то, что клиент после коннекта сам отправит логин,
// если он был залогинен. Но если бы была проверка сессии, логика была бы тут.
// Добавляем вызов handleRequestGameState при коннекте, если есть user data (для примера,
// но для полной реализации нужны cookies/токены)
// if (socket.userData?.userId) { // Эта проверка сработает только после успешного логина в текущей сессии
// gameManager.handleRequestGameState(socket, socket.userData.userId); // Передаем объект socket
// }
// --- Обработчики событий Аутентификации ---
socket.on('register', async (data) => {
console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`);
const result = await auth.registerUser(data?.username, data?.password);
if (result.success) {
console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`);
} else {
console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`);
}
socket.emit('registerResponse', result);
}); });
socket.on('login', async (data) => {
console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`);
const result = await auth.loginUser(data?.username, data?.password);
if (result.success) {
console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`);
// Сохраняем информацию о пользователе в сессии сокета
socket.userData = { userId: result.userId, username: result.username };
loggedInUsers[socket.id] = socket.userData;
// Проверяем, есть ли у пользователя активная игра при логине (если он был отключен)
// ИСПРАВЛЕНИЕ: Передаем объект socket
gameManager.handleRequestGameState(socket, socket.userData.userId);
} else {
console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`);
socket.userData = null; // Убеждаемся, что данные пользователя на сокете сброшены
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
}
socket.emit('loginResponse', result);
});
socket.on('logout', () => {
console.log(`[Socket.IO] Logout for user ${socket.userData?.username || socket.id}`);
// Уведомляем gameManager о дисконнекте (для корректного выхода из игры, если в ней был)
// Game Manager сам очистит ссылку socketToGame[socket.id] при handleDisconnect
// ИСПРАВЛЕНИЕ: Передаем userId или socket.id в handleDisconnect
gameManager.handleDisconnect(socket.id, socket.userData?.userId || socket.id);
// Очищаем информацию о пользователе на сокете и в хранилище
socket.userData = null;
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
// Клиент должен сам переключиться на экран аутентификации
});
// --- Обработчики событий Управления Играми ---
socket.on('createGame', (data) => {
// Пользователь, даже не залогиненный, может создать AI игру (идентифицируется по socket.id)
// Для PvP игры нужна аутентификация (идентификация по userId)
const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей
const mode = data?.mode || 'ai';
if (mode === 'pvp' && !socket.userData) {
socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' });
return;
}
console.log(`[Socket.IO] Create Game request from ${socket.userData?.username || socket.id} (Identifier: ${identifier}). Mode: ${mode}, Character: ${data?.characterKey}`);
const characterKey = data?.characterKey || 'elena'; // По умолчанию Елена
gameManager.createGame(socket, mode, characterKey, identifier); // Передаем идентификатор
});
socket.on('joinGame', (data) => {
if (!socket.userData) { // Проверяем, залогинен ли пользователь
socket.emit('gameError', { message: 'Необходимо войти в систему для присоединения к игре.' });
return;
}
console.log(`[Socket.IO] Join Game request from ${socket.userData.username} (${socket.id}). Game ID: ${data?.gameId}`);
const gameId = data?.gameId;
const identifier = socket.userData.userId; // Присоединиться может только залогиненный
if (gameId) {
gameManager.joinGame(socket, gameId, identifier); // Передаем идентификатор
} else {
socket.emit('gameError', { message: 'Не указан ID игры для присоединения.' });
}
});
socket.on('findRandomGame', (data) => {
if (!socket.userData) { // Проверяем, залогинен ли пользователь
socket.emit('gameError', { message: 'Необходимо войти в систему для поиска игры.' });
return;
}
console.log(`[Socket.IO] Find Random Game request from ${socket.userData.username} (${socket.id}). Preferred Character: ${data?.characterKey}`);
const characterKey = data?.characterKey || 'elena'; // Предпочитаемый персонаж для создания, если не найдено
const identifier = socket.userData.userId; // Ищет и создает только залогиненный
gameManager.findAndJoinRandomPvPGame(socket, characterKey, identifier); // Передаем идентификатор
});
socket.on('requestPvPGameList', () => {
// Список игр доступен всем, даже не залогиненным, но присоединиться можно только залогиненным
// if (!socket.userData) {
// socket.emit('gameError', { message: 'Необходимо войти в систему для просмотра игр.' });
// return;
// }
console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`);
const availableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', availableGames);
});
// Обработчик для клиента, запрашивающего состояние игры (например, при переподключении)
socket.on('requestGameState', () => {
// Запрашивать состояние игры может только залогиненный пользователь, т.к. только у них есть userId для идентификации
if (!socket.userData) {
console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`);
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
return;
}
console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (${socket.id}).`);
// ИСПРАВЛЕНИЕ: Передаем объект socket и identifier (userId)
gameManager.handleRequestGameState(socket, socket.userData.userId);
});
// --- Обработчик события Игрового Действия ---
socket.on('playerAction', (actionData) => {
// Действие в игре может совершить как залогиненный (PvP), так и не залогиненный (AI) игрок.
// Используем userId для залогиненных, socket.id для гостей.
const identifier = socket.userData?.userId || socket.id;
// Game Manager сам проверит, находится ли идентификатор в игре и его ли сейчас ход
// ИСПРАВЛЕНИЕ: Передаем идентификатор вместо socket.id
gameManager.handlePlayerAction(identifier, actionData); // Передаем идентификатор
});
// --- Обработчик отключения сокета ---
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей console.log(`[BC App Socket.IO] User disconnected: ${socket.id} (Reason: ${reason})`);
console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`);
// Уведомляем gameManager о дисконнекте, чтобы он обновил состояние игры.
// Передаем идентификатор пользователя.
gameManager.handleDisconnect(socket.id, identifier); // Передаем как socketId, так и identifier
// Удаляем пользователя из списка залогиненных, если был там
if (loggedInUsers[socket.id]) {
delete loggedInUsers[socket.id];
}
// Если сокет не был залогинен, его identifier был socket.id.
// Связь userIdentifierToGameId будет очищена в gameManager.handleDisconnect, если игра пуста.
}); });
// Опционально: отправка списка активных игр на сервере для отладки (по запросу с консоли или админки) // ... ваши обработчики auth, GameManager и т.д. ...
// global.getActiveGames = () => gameManager.getActiveGamesList(); // Убедитесь, что они не полагаются на префикс пути /battleclub/ во внутренних данных,
// console.log("Type getActiveGames() in server console to list games."); // так как Express его "не видит" после проксирования Apache.
}); });
// Запуск HTTP сервера // Запуск HTTP сервера
const PORT = process.env.PORT || 3200; // Использовать порт из переменных окружения или 3000 по умолчанию server.listen(BC_APP_INTERNAL_PORT, BC_APP_INTERNAL_HOST, () => {
server.listen(PORT, () => { console.log(`Battle Club HTTP Application Server running at http://${BC_APP_INTERNAL_HOST}:${BC_APP_INTERNAL_PORT}`);
console.log(`Server running on port ${PORT}`); console.log(`Socket.IO will be available via proxy at path: ${PUBLIC_PATH_PREFIX}/socket.io`);
console.log(`Serving static files from: ${path.join(__dirname, 'public')}`); console.log(`Serving static files from: ${path.join(__dirname, 'public')}`);
// console.log("Database connection pool created/checked (from db.js require)."); // db.js уже логирует
}); });
// Обработка необработанных промис-ошибок // Обработчики глобальных ошибок
process.on('unhandledRejection', (reason, promise) => { process.on('unhandledRejection', (reason, promise) => {
console.error('[UNHANDLED REJECTION] Unhandled Rejection at:', promise, 'reason:', reason); console.error('[BC App UNHANDLED REJECTION] At:', promise, 'reason:', reason);
// Логировать ошибку, возможно, завершить процесс в продакшене
}); });
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
console.error('[UNCAUGHT EXCEPTION] Caught exception:', err); console.error('[BC App UNCAUGHT EXCEPTION] Caught exception:', err);
// Логировать ошибку, выполнить очистку ресурсов, и завершить процесс // process.exit(1); // В продакшене может быть оправдано
// В продакшене здесь может быть более сложная логика, например, graceful shutdown
// process.exit(1); // Аварийное завершение процесса - можно раскомментировать в продакшене
}); });

4
deploy-script.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
cd /home/nodejs/bc/ || exit 1
git pull origin main
pm2 restart /home/nodejs/bc/bc.js

9
node_modules/child_process/README.md generated vendored Normal file
View File

@ -0,0 +1,9 @@
# Security holding package
This package name is not currently in use, but was formerly occupied
by another package. To avoid malicious use, npm is hanging on to the
package name, but loosely, and we'll probably give it to you if you
want it.
You may adopt this package by contacting support@npmjs.com and
requesting the name.

20
node_modules/child_process/package.json generated vendored Normal file
View File

@ -0,0 +1,20 @@
{
"name": "child_process",
"version": "1.0.2",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/npm/security-holder.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/npm/security-holder/issues"
},
"homepage": "https://github.com/npm/security-holder#readme"
}

7
node_modules/crypto/README.md generated vendored Normal file
View File

@ -0,0 +1,7 @@
# Deprecated Package
This package is no longer supported and has been deprecated. To avoid malicious use, npm is hanging on to the package name.
It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.
Please contact support@npmjs.com if you have questions about this package.

19
node_modules/crypto/package.json generated vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "crypto",
"version": "1.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/npm/deprecate-holder.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/npm/deprecate-holder/issues"
},
"homepage": "https://github.com/npm/deprecate-holder#readme"
}

View File

@ -214,7 +214,7 @@
</div> </div>
</div> <!-- Конец .game-wrapper --> </div> <!-- Конец .game-wrapper -->
<script src="/socket.io/socket.io.js"></script> <script src="/battleclub/socket.io/socket.io.js"></script>
<script src="./js/ui.js"></script> <script src="./js/ui.js"></script>
<script src="./js/client.js"></script> <script src="./js/client.js"></script>
</body> </body>

View File

@ -1,9 +1,7 @@
// /public/js/client.js // /public/js/client.js
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const socket = io({ const socket = io({ path: '/battleclub/socket.io' })
// Опции Socket.IO, если нужны
});
// --- Состояние клиента --- // --- Состояние клиента ---
let currentGameState = null; let currentGameState = null;

116
webhook.js Normal file
View File

@ -0,0 +1,116 @@
// webhook-receiver.js
const https = require('https'); // Используем https
const fs = require('fs'); // Для чтения файлов сертификатов
const express = require('express');
const crypto = require('crypto'); // Для проверки секрета Gitea (если будете использовать)
const { exec } = require('child_process'); // Для запуска скрипта деплоя
const path = require('path'); // Убедитесь, что path импортирован в начале файла
const app = express();
const port = 3800; // Порт, на котором слушает этот сервис
const ipAddress = "81.177.140.16"; // Ваш IP-адрес, на котором слушать
// --- Ваши переменные ---
const GITEA_SECRET = 'Hjp"f2mWF]3>Mc'; // Ваш секрет Gitea (используется для проверки подписи)
const DEPLOY_SCRIPT_PATH = path.join(__dirname, 'deploy-script.sh');
// --- Опции для HTTPS сервера ---
// Убедитесь, что пути к сертификатам верны и у Node.js есть права на их чтение
const sslOptions = {
key: fs.readFileSync('/etc/letsencrypt/live/pavel-chagovsky.com/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/pavel-chagovsky.com/fullchain.pem'),
};
// --- Middlewares ---
app.use(express.json()); // Для парсинга JSON тела запроса
// app.use(express.urlencoded({ extended: true })); // Если будете принимать form-urlencoded данные
// --- Маршруты ---
app.post('/', (req, res) => { // Слушаем POST-запросы на корневой путь "/"
console.log(`[${new Date().toISOString()}] --- HTTPS POST Request to / Received ---`);
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// Опционально: Проверка секрета, если запрос от Gitea
// Эту часть можно закомментировать, если вы тестируете просто с cURL из PHP без секрета
const giteaSignature = req.headers['x-gitea-signature'];
if (GITEA_SECRET && giteaSignature) { // Проверяем только если секрет задан и подпись пришла
const hmac = crypto.createHmac('sha256', GITEA_SECRET);
// Важно: Gitea подписывает СЫРОЕ тело запроса.
// express.json() уже распарсил req.body. Для точной проверки подписи нужно использовать сырое тело.
// Для этого можно использовать middleware типа `raw-body` или настроить express.json с опцией `verify`.
// Пока для простоты пропустим точную проверку подписи, но в продакшене это ВАЖНО.
// Просто для примера, как это могло бы быть (требует доработки с raw body):
// const digest = 'sha256=' + hmac.update(JSON.stringify(req.body) /* НЕПРАВИЛЬНО для проверки подписи Gitea, нужно сырое тело */).digest('hex');
// if (!crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(giteaSignature))) {
// console.warn(`[${new Date().toISOString()}] Webhook: Invalid signature`);
// return res.status(401).send('Invalid signature');
// }
console.log(`[${new Date().toISOString()}] Webhook: Gitea signature present (проверка пока упрощена).`);
} else if (GITEA_SECRET && !giteaSignature) {
console.warn(`[${new Date().toISOString()}] Webhook: Missing Gitea signature, but secret is configured.`);
// return res.status(401).send('Missing signature'); // Можно раскомментировать для строгости
}
// Запуск скрипта деплоя
console.log(`[${new Date().toISOString()}] Executing deploy script: ${DEPLOY_SCRIPT_PATH}`);
exec(DEPLOY_SCRIPT_PATH, (error, stdout, stderr) => {
if (error) {
console.error(`[${new Date().toISOString()}] Deploy Script Error: ${error.message}`);
console.error(`[${new Date().toISOString()}] Deploy Script Stderr: ${stderr}`);
// Не отправляем ошибку клиенту сразу, чтобы не раскрывать детали,
// но можно отправить общий код ошибки.
if (!res.headersSent) {
return res.status(500).send('Deployment script failed to execute.');
}
return;
}
if (stderr) {
console.warn(`[${new Date().toISOString()}] Deploy Script Stderr: ${stderr}`);
}
console.log(`[${new Date().toISOString()}] Deploy Script Stdout: ${stdout}`);
if (!res.headersSent) {
res.status(200).send('Webhook received and deployment script initiated.');
}
});
});
// GET-маршрут для простой проверки, что сервер жив
app.get('/', (req, res) => {
console.log(`[${new Date().toISOString()}] --- HTTPS GET Request to / Received ---`);
res.status(200).send('Express HTTPS server is running.');
});
// --- Запуск HTTPS сервера ---
const server = https.createServer(sslOptions, app);
server.listen(port, ipAddress, () => {
console.log(`[${new Date().toISOString()}] Express HTTPS server listening at https://${ipAddress}:${port}`);
});
server.on('error', (err) => {
console.error(`[${new Date().toISOString()}] Server critical error:`, err);
if (err.code === 'EADDRINUSE') {
console.error(`[${new Date().toISOString()}] Port ${port} on IP ${ipAddress} is already in use.`);
}
// process.exit(1); // Можно завершить процесс при критической ошибке сервера
});
// Обработка сигналов для корректного завершения (опционально, но хорошо для pm2/systemd)
function gracefulShutdown(signal) {
console.log(`[${new Date().toISOString()}] Received ${signal}. Shutting down server...`);
server.close(() => {
console.log(`[${new Date().toISOString()}] HTTP server closed.`);
// Здесь можно добавить закрытие других ресурсов, если они есть
process.exit(0);
});
// Если сервер не закрывается за таймаут, принудительно завершить
setTimeout(() => {
console.error(`[${new Date().toISOString()}] Could not close connections in time, forcefully shutting down.`);
process.exit(1);
}, 10000); // 10 секунд
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C