diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..5fbc2b8 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,52 @@ +Version: 0.2 +- compose.yaml + -- убран внешний порт для БД + +- docker_build.sh + -- файл инициализации БД https://github.com/Lineage2JS/scripts/blob/main/l2db.sql + +- docker/* + -- образ в сборке заменен на node:lts-alpine3.23 + +TODO: +- Добавить ENV-файл для чувствительных данных + +------ + +**Установка docker и git** +``` +apt install docker.id docker-compose git curl +``` +``` +sudo usermod -aG docker $USER +``` + +**Копируем файлы для сборки** +``` +git clone https://github.com/Lineage2JS/docs.git +``` + +**Запуск скрипта сборки** +``` +cp -r docs/docker ./docker + +rm -rf docs + +cd docker + +chmod +x docker_build.sh + +./docker_build.sh +``` + +**Запуск сборки** +``` +docker compose up -d + +docker compose logs -f + +docker compose stop + +docker compose down +``` + diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 0000000..e5b83b3 --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,68 @@ +services: + + database: + image: postgres:17.7 + restart: unless-stopped + environment: + POSTGRES_USER: l2js-user + POSTGRES_PASSWORD: l2js-passwd + POSTGRES_DB: l2js-db + volumes: + - database:/var/lib/postgresql/data + - ./docker/initdb:/docker-entrypoint-initdb.d + + game-server: + image: game-server:latest + restart: unless-stopped + environment: + GS_HOST: game-server + GS_PORT: '7777' + DB_USER: l2js-user + DB_PASSWORD: l2js-passwd + DB_NAME: l2js-db + DB_HOST: database + DB_PORT: '5432' + ports: + - "7777:7777" + depends_on: + - database + + login-server: + image: login-server:latest + restart: unless-stopped + environment: + LS_HOST: login-server + LS_PORT: '2106' + DB_USER: l2js-user + DB_PASSWORD: l2js-passwd + DB_NAME: l2js-db + DB_HOST: database + DB_PORT: '5432' + ports: + - "2106:2106" + depends_on: + - database + + web-server: + image: web-server:latest + restart: unless-stopped + environment: + WS_PORT: '80' + LS_HOST: login-server + LS_PORT: '2106' + GS_HOST: game-server + GS_PORT: '2106' + DB_USER: l2js-user + DB_PASSWORD: l2js-passwd + DB_NAME: l2js-db + DB_HOST: database + DB_PORT: '5432' + STATIC_FILES_PATH: '/var/www/html/lineage2js-web-ui/' + ports: + - "8000:80" + depends_on: + - login-server + - game-server + +volumes: + database: diff --git a/docker/docker/dockerignore b/docker/docker/dockerignore new file mode 100644 index 0000000..051ed09 --- /dev/null +++ b/docker/docker/dockerignore @@ -0,0 +1,3 @@ +.git +.gitignore +.dockerignore diff --git a/docker/docker/game-server/Dockerfile b/docker/docker/game-server/Dockerfile new file mode 100644 index 0000000..ada3e7b --- /dev/null +++ b/docker/docker/game-server/Dockerfile @@ -0,0 +1,11 @@ +FROM node:lts-alpine3.23 + +WORKDIR /opt/lineage2js-game-server + +COPY package*.json ./ + +RUN npm ci --only=production + +COPY . . + +CMD ["node", "server.js"] diff --git a/docker/docker/game-server/config/database.js b/docker/docker/game-server/config/database.js new file mode 100644 index 0000000..5a3998e --- /dev/null +++ b/docker/docker/game-server/config/database.js @@ -0,0 +1,7 @@ +module.exports = { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + dbname: process.env.DB_NAME, +} diff --git a/docker/docker/game-server/config/gameserver.js b/docker/docker/game-server/config/gameserver.js new file mode 100644 index 0000000..f9c7cf3 --- /dev/null +++ b/docker/docker/game-server/config/gameserver.js @@ -0,0 +1,8 @@ +module.exports = { + id: 1, + host: process.env.GS_HOST, + port: process.env.GS_PORT, + ageLimit: 0, + isPvP: false, + maxPlayers: 100 +} diff --git a/docker/docker/login-server/Dockerfile b/docker/docker/login-server/Dockerfile new file mode 100644 index 0000000..444b98f --- /dev/null +++ b/docker/docker/login-server/Dockerfile @@ -0,0 +1,11 @@ +FROM node:lts-alpine3.23 + +WORKDIR /opt/lineage2js-login-server + +COPY package*.json ./ + +RUN npm ci --only=production + +COPY . . + +CMD ["node", "server.js"] diff --git a/docker/docker/login-server/config/database.js b/docker/docker/login-server/config/database.js new file mode 100644 index 0000000..5a3998e --- /dev/null +++ b/docker/docker/login-server/config/database.js @@ -0,0 +1,7 @@ +module.exports = { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + dbname: process.env.DB_NAME, +} diff --git a/docker/docker/login-server/config/loginserver.js b/docker/docker/login-server/config/loginserver.js new file mode 100644 index 0000000..2d2ae39 --- /dev/null +++ b/docker/docker/login-server/config/loginserver.js @@ -0,0 +1,4 @@ +module.exports = { + host: process.env.LS_HOST, + port: process.env.LS_PORT, +} diff --git a/docker/docker/web-server/Dockerfile b/docker/docker/web-server/Dockerfile new file mode 100644 index 0000000..54179df --- /dev/null +++ b/docker/docker/web-server/Dockerfile @@ -0,0 +1,26 @@ +FROM node:lts-alpine3.23 AS builder + +WORKDIR /opt/lineage2js-web-ui + +COPY . . + +RUN cd web-ui && \ + npm ci --only=production && \ + npm install && \ + npm run build + +FROM node:lts-alpine3.23 + +WORKDIR /opt/lineage2js-web-server + +COPY package*.json ./ + +RUN npm ci --only=production + +COPY . . + +COPY --from=builder /opt/lineage2js-web-ui/web-ui/dist/ /var/www/html/lineage2js-web-ui/ + +RUN rm -R web-ui/ + +CMD ["node", "server.js"] diff --git a/docker/docker/web-server/server.js b/docker/docker/web-server/server.js new file mode 100644 index 0000000..7bfe1c9 --- /dev/null +++ b/docker/docker/web-server/server.js @@ -0,0 +1,240 @@ +const express = require('express'); +const cors = require('cors'); +const net = require('net'); +const svgCaptcha = require('svg-captcha'); +const { json } = require('body-parser'); +const { Pool } = require('pg'); +const dotenv = require('dotenv'); + +const server = express(); +const pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, +}); + +server.use(cors()); +server.use(json()); + +if (process.env.NODE_ENV !== 'production') { + dotenv.config(); +} + +const staticPath = process.env.STATIC_FILES_PATH || 'public'; + +server.use(express.static(staticPath)); + +async function checkAccountExists(login) { + try { + const result = await pool.query( + 'SELECT id FROM accounts WHERE login = $1', + [login] + ); + return result.rows.length > 0; + } catch (error) { + console.error('Error checking account existence:', error); + throw error; + } +} + +const captchaStore = new Map(); + +server.get('/captcha', (request, response) => { + const captcha = svgCaptcha.create({ + size: 5, + ignoreChars: '0o1iIl', + noise: 2, + color: true, + background: '#f0f0f0' + }); + + const captchaId = Date.now().toString(); + + // Сохраняем текст капчи + captchaStore.set(captchaId, captcha.text.toLowerCase()); + + // Удаляем капчу через 10 минут + setTimeout(() => { + captchaStore.delete(captchaId); + }, 10 * 60 * 1000); + + response.json({ + status: 'success', + data: { + captchaId, + captcha: captcha.data + } + }); +}); + +server.post('/account', async (request, response) => { + const { login, password, captchaId, captchaCode } = request.body; + + const storedText = captchaStore.get(captchaId); + + if (!storedText) { + return response.json({ + status: 'failed', + message: 'Сaptcha is outdated' + }); + + return; + } + + if (captchaCode.toLowerCase() != storedText) { + response.status(400).json({ + status: 'failed', + message: 'Invalid captcha' + }); + + return; + } + + // Удаляем использованную капчу + captchaStore.delete(captchaId); + + if (!login || !password) { + response.status(400).json({ + status: 'failed', + message: 'Login and password are required' + }); + + return + } + + if (login.length < 1 || password.length < 1) { + response.status(400).json({ + status: 'failed', + message: 'Login must be at least 3 characters and password at least 6 characters' + }); + + return + } + + try { + const accountExists = await checkAccountExists(login); + + if (accountExists) { + response.status(409).json({ + status: 'failed', + message: 'Account with this login already exists' + }); + + return; + } + + const result = await pool.query( + 'INSERT INTO accounts (login, password) VALUES ($1, $2) RETURNING id', + [login, password] + ); + + response.status(201).json({ + status: 'success', + message: 'Account created successfully', + }); + + } catch (error) { + if (error.code === '23505') { + response.status(409).json({ + status: 'failed', + message: 'Account with this login already exists' + }); + } else { + console.error('Registration error:', error); + response.status(500).json({ + status: 'failed', + message: 'Internal server error' + }); + } + } +}); + +const loginServerStatus = { + host: process.env.LS_HOST, + port: process.env.LS_PORT, + status: 'unknown', // 'up', 'down', 'unknown', 'checking' + error: null, +}; +const gameServerStatus = { + host: process.env.GS_HOST, + port: process.env.GS_PORT, + status: 'unknown', // 'up', 'down', 'unknown', 'checking' + error: null, +}; + +async function checkTcpServer(host, port) { + return new Promise((resolve) => { + const client = new net.Socket(); + const timeout = 3000; + const timer = setTimeout(() => { + client.destroy(); + resolve({ + status: 'down', + error: 'Connection timeout' + }); + }, timeout); + + client.connect(port, host, () => { + clearTimeout(timer); + client.end(); + resolve({ + status: 'up' + }); + }); + + client.on('error', (err) => { + clearTimeout(timer); + resolve({ + status: 'down', + error: err.message + }); + }); + }); +} + +async function updateTcpStatus(serverStatus) { + serverStatus.status = 'checking'; + + try { + const result = await checkTcpServer(serverStatus.host, serverStatus.port); + + serverStatus.status = result.status; + serverStatus.error = result.error; + } catch (error) { + serverStatus.status = 'error'; + serverStatus.error = error.message; + } +} + +function startTcpPolling() { + setInterval(() => updateTcpStatus(loginServerStatus), 3000); + setInterval(() => updateTcpStatus(gameServerStatus), 3000); +} + +startTcpPolling(); + +server.get('/status/:serverType/', (request, response) => { + const serverType = request.params.serverType; + + if (serverType === 'login') { + const payload = { + status: 'success', + data: loginServerStatus.status + } + + response.json(payload); + } + + if (serverType === 'game') { + const payload = { + status: 'success', + data: gameServerStatus.status + } + + response.json(payload); + } +}); + +server.listen(process.env.WS_PORT); diff --git a/docker/docker_build.sh b/docker/docker_build.sh new file mode 100755 index 0000000..828227d --- /dev/null +++ b/docker/docker_build.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +echo "Downloading files..." +echo "" +repos=( + "https://github.com/Lineage2JS/game-server.git" + "https://github.com/Lineage2JS/login-server.git" + "https://github.com/Lineage2JS/web-server.git" + "https://github.com/Lineage2JS/web-ui.git" +) +for repo in "${repos[@]}"; do + git clone "$repo" +done + +echo "" +echo "Copy docker files..." +echo "" +for module in game-server login-server web-server; do + cp "docker/${module}/Dockerfile" "${module}/Dockerfile" + cp docker/dockerignore "${module}/.dockerignore" +done + +echo "" +echo "Merge web-server and web-ui..." +echo "" +mv web-ui web-server/ + +echo "" +echo "Copy l2js files..." +echo "" +cp docker/game-server/config/* game-server/config/ +cp docker/login-server/config/* login-server/config/ +cp docker/web-server/server.js web-server/ +mkdir -p docker/initdb +curl -o docker/initdb/l2db.sql https://raw.githubusercontent.com/Lineage2JS/scripts/refs/heads/main/l2db.sql + +echo "" +echo "Building docker images..." +echo "" +for module in game-server login-server web-server; do + docker build -t "${module}:latest" "${module}/" +done