From ea3c2e750c66c19b6b87ad964e4f3d6b65c4a257 Mon Sep 17 00:00:00 2001 From: uprightbass360 Date: Fri, 12 Dec 2025 18:33:53 -0500 Subject: [PATCH] adds pdump and 2fa generation --- docker-compose.yml | 4 + docs/SCRIPTS.md | 121 ++++ import/README.md | 29 +- import/pdumps/README.md | 192 ++++++ .../pdumps/examples/batch-import.sh.example | 43 ++ import/pdumps/examples/character.conf.example | 20 + scripts/bash/bulk-2fa-setup.sh | 586 ++++++++++++++++++ scripts/bash/generate-2fa-qr.py | 116 ++++ scripts/bash/generate-2fa-qr.sh | 166 +++++ scripts/bash/import-pdumps.sh | 283 +++++++++ scripts/bash/pdump-import.sh | 344 ++++++++++ scripts/bash/test-2fa-token.py | 65 ++ 12 files changed, 1968 insertions(+), 1 deletion(-) create mode 100644 import/pdumps/README.md create mode 100755 import/pdumps/examples/batch-import.sh.example create mode 100644 import/pdumps/examples/character.conf.example create mode 100755 scripts/bash/bulk-2fa-setup.sh create mode 100755 scripts/bash/generate-2fa-qr.py create mode 100755 scripts/bash/generate-2fa-qr.sh create mode 100755 scripts/bash/import-pdumps.sh create mode 100755 scripts/bash/pdump-import.sh create mode 100755 scripts/bash/test-2fa-token.py diff --git a/docker-compose.yml b/docker-compose.yml index 44ef77c..d08dd8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: MYSQL_MAX_CONNECTIONS: ${MYSQL_MAX_CONNECTIONS} MYSQL_INNODB_BUFFER_POOL_SIZE: ${MYSQL_INNODB_BUFFER_POOL_SIZE} MYSQL_INNODB_LOG_FILE_SIZE: ${MYSQL_INNODB_LOG_FILE_SIZE} + MYSQL_BINLOG_EXPIRE_LOGS_SECONDS: 86400 TZ: "${TZ}" entrypoint: - /usr/local/bin/mysql-entrypoint.sh @@ -46,6 +47,9 @@ services: - --innodb-buffer-pool-size=${MYSQL_INNODB_BUFFER_POOL_SIZE} - --innodb-log-file-size=${MYSQL_INNODB_LOG_FILE_SIZE} - --innodb-redo-log-capacity=${MYSQL_INNODB_REDO_LOG_CAPACITY} + - --expire_logs_days=0 + - --binlog_expire_logs_seconds=86400 + - --binlog_expire_logs_auto_purge=ON restart: unless-stopped logging: *logging-default healthcheck: diff --git a/docs/SCRIPTS.md b/docs/SCRIPTS.md index a862955..b3a301a 100644 --- a/docs/SCRIPTS.md +++ b/docs/SCRIPTS.md @@ -140,6 +140,127 @@ Restores user accounts and characters from backup while preserving world data. - `acore_characters.sql[.gz]` - Character data (required) - `acore_world.sql[.gz]` - World data (optional) +#### `scripts/bash/pdump-import.sh` - Character Import +Imports individual character dump files into the database. + +```bash +# Import character from pdump file +./scripts/bash/pdump-import.sh --file character.pdump --account testuser --password azerothcore123 + +# Import with character rename +./scripts/bash/pdump-import.sh --file oldchar.pdump --account newuser --name "NewName" --password azerothcore123 + +# Validate pdump without importing (dry run) +./scripts/bash/pdump-import.sh --file character.pdump --account testuser --password azerothcore123 --dry-run +``` + +**Features:** +- Automatic GUID assignment or manual override with `--guid` +- Character renaming during import with `--name` +- Account validation and character name uniqueness checks +- Automatic database backup before import +- Safe server restart handling + +#### `scripts/bash/import-pdumps.sh` - Batch Character Import +Processes multiple character dump files from the `import/pdumps/` directory. + +```bash +# Import all pdumps with environment settings +./scripts/bash/import-pdumps.sh --password azerothcore123 --account defaultuser + +# Non-interactive batch import +./scripts/bash/import-pdumps.sh --password azerothcore123 --non-interactive +``` + +**Directory Structure:** +``` +import/pdumps/ +├── character1.pdump # Character dump files +├── character2.sql # SQL dump files also supported +├── configs/ # Optional per-character configuration +│ ├── character1.conf # account=user1, name=NewName +│ └── character2.conf # account=user2, guid=5000 +└── processed/ # Successfully imported files moved here +``` + +**Configuration Format (`.conf`):** +```ini +account=target_account_name_or_id +name=new_character_name # Optional: rename character +guid=force_specific_guid # Optional: force GUID +``` + +### Security Management Scripts + +#### `scripts/bash/bulk-2fa-setup.sh` - Bulk 2FA Setup +Configures TOTP 2FA for multiple AzerothCore accounts using official SOAP API. + +```bash +# Setup 2FA for all accounts without it +./scripts/bash/bulk-2fa-setup.sh --all + +# Setup for specific accounts +./scripts/bash/bulk-2fa-setup.sh --account user1 --account user2 + +# Force regenerate with custom issuer +./scripts/bash/bulk-2fa-setup.sh --all --force --issuer "MyServer" + +# Preview what would be done +./scripts/bash/bulk-2fa-setup.sh --all --dry-run + +# Use custom SOAP credentials +./scripts/bash/bulk-2fa-setup.sh --all --soap-user admin --soap-pass adminpass +``` + +**Features:** +- **Official AzerothCore API Integration**: Uses SOAP commands instead of direct database manipulation +- Generates AzerothCore-compatible 16-character Base32 TOTP secrets (longer secrets are rejected by SOAP) +- Automatic account discovery or specific targeting +- QR code generation for authenticator apps +- Force regeneration of existing 2FA secrets +- Comprehensive output with setup instructions +- Safe dry-run mode for testing +- SOAP connectivity validation +- Proper error handling and validation + +**Requirements:** +- AzerothCore worldserver with SOAP enabled (SOAP.Enabled = 1) +- SOAP port exposed on 7778 (SOAP.Port = 7878, mapped to external 7778) +- Remote Access enabled (Ra.Enable = 1) in worldserver.conf +- SOAP.IP = "0.0.0.0" for external connectivity +- GM account with sufficient privileges (gmlevel 3) + +**Output Structure:** +``` +./2fa-setup-TIMESTAMP/ +├── qr-codes/ # QR code images for each account +├── setup-report.txt # Complete setup summary +├── console-commands.txt # Manual verification commands +└── secrets-backup.csv # Secure backup of all secrets +``` + +**Security Notes:** +- Generated QR codes and backup files contain sensitive TOTP secrets +- Distribute QR codes securely to users +- Delete or encrypt backup files after distribution +- TOTP secrets are also stored in AzerothCore database + +#### `scripts/bash/generate-2fa-qr.sh` / `generate-2fa-qr.py` - Individual 2FA Setup +Generate QR codes for individual account 2FA setup. + +```bash +# Generate QR code for single account +./scripts/bash/generate-2fa-qr.sh -u username + +# Use custom issuer and output path +./scripts/bash/generate-2fa-qr.sh -u username -i "MyServer" -o /tmp/qr.png + +# Use existing secret +./scripts/bash/generate-2fa-qr.sh -u username -s JBSWY3DPEHPK3PXP +``` + +> AzerothCore's SOAP endpoint only accepts 16-character Base32 secrets (A-Z and 2-7). The generators enforce this length to avoid "The provided two-factor authentication secret is not valid" errors. + ### Module Management Scripts #### `scripts/bash/stage-modules.sh` - Module Staging diff --git a/import/README.md b/import/README.md index df3afd5..a09c931 100644 --- a/import/README.md +++ b/import/README.md @@ -7,7 +7,8 @@ This directory allows you to easily import custom database files and configurati ``` import/ ├── db/ # Database SQL files to import -└── conf/ # Configuration file overrides +├── conf/ # Configuration file overrides +└── pdumps/ # Character dump files to import ``` ## 🗄️ Database Import (`import/db/`) @@ -93,6 +94,31 @@ AiPlayerbot.MaxRandomBots = 200 See `config/CONFIG_MANAGEMENT.md` for detailed preset documentation. +## 🎮 Character Import (`import/pdumps/`) + +Import character dump files from other AzerothCore servers. + +### Supported Formats +- **`.pdump`** - Character dump files from `.pdump write` command +- **`.sql`** - SQL character dump files + +### Quick Start +1. Place character dump files in `import/pdumps/` +2. Run the import script: + ```bash + ./scripts/bash/import-pdumps.sh --password your_mysql_password --account target_account + ``` + +### Advanced Configuration +Create `import/pdumps/configs/filename.conf` for per-character settings: +```ini +account=target_account +name=NewCharacterName # Optional: rename +guid=5000 # Optional: force GUID +``` + +**📖 For complete character import documentation, see [import/pdumps/README.md](pdumps/README.md)** + ## 🔄 Automated Import Both database and configuration imports are automatically handled during: @@ -118,6 +144,7 @@ Both database and configuration imports are automatically handled during: ## 📚 Related Documentation +- [Character Import Guide](pdumps/README.md) - Complete pdump import documentation - [Database Management](../docs/DATABASE_MANAGEMENT.md) - [Configuration Management](../config/CONFIG_MANAGEMENT.md) - [Module Management](../docs/ADVANCED.md#module-management) diff --git a/import/pdumps/README.md b/import/pdumps/README.md new file mode 100644 index 0000000..be4de6e --- /dev/null +++ b/import/pdumps/README.md @@ -0,0 +1,192 @@ +# Character PDump Import + +This directory allows you to easily import character pdump files into your AzerothCore server. + +## 📁 Directory Structure + +``` +import/pdumps/ +├── README.md # This file +├── *.pdump # Place your character dump files here +├── *.sql # SQL dump files also supported +├── configs/ # Optional per-file configuration +│ ├── character1.conf +│ └── character2.conf +├── examples/ # Example files and configurations +└── processed/ # Successfully imported files are moved here +``` + +## 🎮 Character Dump Import + +### Quick Start + +1. **Place your pdump files** in this directory: + ```bash + cp /path/to/mycharacter.pdump import/pdumps/ + ``` + +2. **Run the import script**: + ```bash + ./scripts/bash/import-pdumps.sh --password your_mysql_password --account target_account + ``` + +3. **Login and play** - your characters are now available! + +### Supported File Formats + +- **`.pdump`** - Character dump files from AzerothCore `.pdump write` command +- **`.sql`** - SQL character dump files + +### Configuration Options + +#### Environment Variables (`.env`) +```bash +# Set default account for all imports +DEFAULT_IMPORT_ACCOUNT=testuser + +# Database credentials (usually already set) +MYSQL_ROOT_PASSWORD=your_mysql_password +ACORE_DB_AUTH_NAME=acore_auth +ACORE_DB_CHARACTERS_NAME=acore_characters +``` + +#### Per-Character Configuration (`configs/filename.conf`) +Create a `.conf` file with the same name as your pdump file to specify custom import options: + +**Example: `configs/mycharacter.conf`** +```ini +# Target account (required if not set globally) +account=testuser + +# Rename character during import (optional) +name=NewCharacterName + +# Force specific GUID (optional, auto-assigned if not specified) +guid=5000 +``` + +### Command Line Usage + +#### Import All Files +```bash +# Use environment settings +./scripts/bash/import-pdumps.sh + +# Override settings +./scripts/bash/import-pdumps.sh --password mypass --account testuser +``` + +#### Import Single File +```bash +# Direct import with pdump-import.sh +./scripts/bash/pdump-import.sh --file character.pdump --account testuser --password mypass + +# With character rename +./scripts/bash/pdump-import.sh --file oldchar.pdump --account newuser --name "NewName" --password mypass + +# Validate before import (dry run) +./scripts/bash/pdump-import.sh --file character.pdump --account testuser --password mypass --dry-run +``` + +## 🛠️ Advanced Features + +### Account Management +- **Account Validation**: Scripts automatically verify that target accounts exist +- **Account ID or Name**: You can use either account names or numeric IDs +- **Interactive Mode**: If no account is specified, you'll be prompted to enter one + +### GUID Handling +- **Auto-Assignment**: Next available GUID is automatically assigned +- **Force GUID**: Use `--guid` parameter or config file to force specific GUID +- **Conflict Detection**: Import fails safely if GUID already exists + +### Character Names +- **Validation**: Character names must follow WoW naming rules (2-12 letters) +- **Uniqueness**: Import fails if character name already exists on server +- **Renaming**: Use `--name` parameter or config file to rename during import + +### Safety Features +- **Automatic Backup**: Characters database is backed up before each import +- **Server Management**: World server is safely stopped/restarted during import +- **Rollback Ready**: Backups are stored in `manual-backups/` directory +- **Dry Run**: Validate imports without actually importing + +## 📋 Import Workflow + +1. **Validation Phase** + - Check file format and readability + - Validate target account exists + - Verify character name availability (if specified) + - Check GUID conflicts + +2. **Pre-Import Phase** + - Create automatic database backup + - Stop world server for safe import + +3. **Processing Phase** + - Process SQL file (update account references, GUID, name) + - Import character data into database + +4. **Post-Import Phase** + - Restart world server + - Verify import success + - Move processed files to `processed/` directory + +## 🚨 Important Notes + +### Before You Import +- **Backup Your Database**: Always backup before importing characters +- **Account Required**: Target account must exist in your auth database +- **Unique Names**: Character names must be unique across the entire server +- **Server Downtime**: World server is briefly restarted during import + +### PDump Limitations +The AzerothCore pdump system has some known limitations: +- **Guild Data**: Guild information is not included in pdump files +- **Module Data**: Some module-specific data (transmog, reagent bank) may not transfer +- **Version Compatibility**: Pdump files from different database versions may have issues + +### Troubleshooting +- **"Account not found"**: Verify account exists in auth database +- **"Character name exists"**: Use `--name` to rename or choose different name +- **"GUID conflicts"**: Use `--guid` to force different GUID or let system auto-assign +- **"Database errors"**: Check that pdump file is compatible with your database version + +## 📚 Examples + +### Basic Import +```bash +# Place file and import +cp character.pdump import/pdumps/ +./scripts/bash/import-pdumps.sh --password mypass --account testuser +``` + +### Batch Import with Configuration +```bash +# Set up multiple characters +cp char1.pdump import/pdumps/ +cp char2.pdump import/pdumps/ + +# Configure individual characters +echo "account=user1" > import/pdumps/configs/char1.conf +echo "account=user2 +name=RenamedChar" > import/pdumps/configs/char2.conf + +# Import all +./scripts/bash/import-pdumps.sh --password mypass +``` + +### Single Character Import +```bash +./scripts/bash/pdump-import.sh \ + --file character.pdump \ + --account testuser \ + --name "MyNewCharacter" \ + --password mypass +``` + +## 🔗 Related Documentation + +- [Database Management](../../docs/DATABASE_MANAGEMENT.md) +- [Backup System](../../docs/TROUBLESHOOTING.md#backup-system) +- [Getting Started Guide](../../docs/GETTING_STARTED.md) \ No newline at end of file diff --git a/import/pdumps/examples/batch-import.sh.example b/import/pdumps/examples/batch-import.sh.example new file mode 100755 index 0000000..68c8dd1 --- /dev/null +++ b/import/pdumps/examples/batch-import.sh.example @@ -0,0 +1,43 @@ +#!/bin/bash +# Example batch import script +# This shows how to import multiple characters with different configurations + +set -euo pipefail + +MYSQL_PASSWORD="your_mysql_password_here" + +echo "Setting up character import batch..." + +# Create character-specific configurations +mkdir -p ../configs + +# Character 1: Import to specific account +cat > ../configs/warrior.conf < ../configs/mage.conf < ../configs/priest.conf < ../warrior.pdump" +echo " mage.pdump -> ../mage.pdump" +echo " priest.pdump -> ../priest.pdump" +echo "" +echo "Then run the import:" +echo " ../../../scripts/bash/import-pdumps.sh --password $MYSQL_PASSWORD" +echo "" +echo "Or import individually:" +echo " ../../../scripts/bash/pdump-import.sh --file ../warrior.pdump --account player1 --password $MYSQL_PASSWORD" \ No newline at end of file diff --git a/import/pdumps/examples/character.conf.example b/import/pdumps/examples/character.conf.example new file mode 100644 index 0000000..5cb2834 --- /dev/null +++ b/import/pdumps/examples/character.conf.example @@ -0,0 +1,20 @@ +# Example character import configuration +# Copy this file to configs/yourcharacter.conf and modify as needed + +# Target account (required if DEFAULT_IMPORT_ACCOUNT is not set) +# Can be account name or account ID +account=testuser + +# Rename character during import (optional) +# Must follow WoW naming rules: 2-12 letters, no numbers/special chars +name=NewCharacterName + +# Force specific character GUID (optional) +# If not specified, next available GUID will be used automatically +# guid=5000 + +# Additional notes: +# - Account must exist in auth database before import +# - Character names must be unique across the server +# - GUID conflicts will cause import to fail +# - Use dry-run mode to test before actual import \ No newline at end of file diff --git a/scripts/bash/bulk-2fa-setup.sh b/scripts/bash/bulk-2fa-setup.sh new file mode 100755 index 0000000..a8e20cf --- /dev/null +++ b/scripts/bash/bulk-2fa-setup.sh @@ -0,0 +1,586 @@ +#!/bin/bash +# +# AzerothCore Bulk 2FA Setup Script +# Generates and configures TOTP 2FA for multiple accounts +# +# Usage: ./scripts/bash/bulk-2fa-setup.sh [OPTIONS] +# + +set -e + +# Script directory for relative imports +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source common utilities +source "$SCRIPT_DIR/lib/common.sh" + +# Set environment paths +ENV_PATH="${ENV_PATH:-$PROJECT_ROOT/.env}" +DEFAULT_ENV_PATH="$PROJECT_ROOT/.env" + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= + +# Command line options +OPT_ALL=false +OPT_ACCOUNTS=() +OPT_FORCE=false +OPT_OUTPUT_DIR="" +OPT_DRY_RUN=false +OPT_ISSUER="AzerothCore" +OPT_FORMAT="qr" + +# Container and database settings +WORLDSERVER_CONTAINER="ac-worldserver" +DATABASE_CONTAINER="ac-mysql" +MYSQL_PASSWORD="" + +# SOAP settings for official AzerothCore API +SOAP_HOST="localhost" +SOAP_PORT="7778" +SOAP_USERNAME="" +SOAP_PASSWORD="" + +# Output paths +OUTPUT_BASE_DIR="" +QR_CODES_DIR="" +SETUP_REPORT="" +CONSOLE_COMMANDS="" +SECRETS_BACKUP="" + +# ============================================================================= +# USAGE AND HELP +# ============================================================================= + +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Bulk 2FA setup for AzerothCore accounts using official SOAP API" + echo "" + echo "Options:" + echo " --all Process all non-bot accounts without 2FA" + echo " --account USERNAME Process specific account (can be repeated)" + echo " --force Regenerate 2FA even if already exists" + echo " --output-dir PATH Custom output directory" + echo " --dry-run Show what would be done without executing" + echo " --issuer NAME Issuer name for TOTP (default: AzerothCore)" + echo " --format [qr|manual] Output QR codes or manual setup info" + echo " --soap-user USERNAME SOAP API username (default: from .env)" + echo " --soap-pass PASSWORD SOAP API password (default: from .env)" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 --all # Setup 2FA for all accounts" + echo " $0 --account user1 --account user2 # Setup for specific accounts" + echo " $0 --all --force --issuer MyServer # Force regenerate with custom issuer" + echo " $0 --all --dry-run # Preview what would be done" + echo "" + echo "Requirements:" + echo " - AzerothCore worldserver with SOAP enabled on port 7778" + echo " - GM account with sufficient privileges for SOAP access" + echo " - Remote Access (Ra.Enable = 1) enabled in worldserver.conf" +} + +# ============================================================================= +# UTILITY FUNCTIONS +# ============================================================================= + +# Check if required containers are running and healthy +check_containers() { + info "Checking container status..." + + # Check worldserver container + if ! docker ps --format '{{.Names}}' | grep -q "^${WORLDSERVER_CONTAINER}$"; then + fatal "Container $WORLDSERVER_CONTAINER is not running" + fi + + # Check if database container exists + if ! docker ps --format '{{.Names}}' | grep -q "^${DATABASE_CONTAINER}$"; then + fatal "Container $DATABASE_CONTAINER is not running" + fi + + # Test database connectivity + if ! docker exec "$WORLDSERVER_CONTAINER" mysql -h "$DATABASE_CONTAINER" -u root -p"$MYSQL_PASSWORD" acore_auth -e "SELECT 1;" &>/dev/null; then + fatal "Cannot connect to AzerothCore database" + fi + + # Test SOAP connectivity (only if credentials are available) + if [ -n "$SOAP_USERNAME" ] && [ -n "$SOAP_PASSWORD" ]; then + info "Testing SOAP API connectivity..." + if ! soap_result=$(soap_execute_command "server info"); then + fatal "Cannot connect to SOAP API: $soap_result" + fi + ok "SOAP API is accessible" + fi + + ok "Containers are healthy and accessible" +} + +# Execute MySQL query via container +mysql_query() { + local query="$1" + local database="${2:-acore_auth}" + + docker exec "$WORLDSERVER_CONTAINER" mysql \ + -h "$DATABASE_CONTAINER" \ + -u root \ + -p"$MYSQL_PASSWORD" \ + "$database" \ + -e "$query" \ + 2>/dev/null +} + +# Execute SOAP command via AzerothCore official API +soap_execute_command() { + local command="$1" + local response + + # Construct SOAP XML request + local soap_request=' + + + + '"$command"' + + +' + + # Execute SOAP request + response=$(curl -s -X POST \ + -H "Content-Type: text/xml" \ + --user "$SOAP_USERNAME:$SOAP_PASSWORD" \ + -d "$soap_request" \ + "http://$SOAP_HOST:$SOAP_PORT/" 2>/dev/null) + + # Flatten response for reliable parsing + local flat_response + flat_response=$(echo "$response" | tr -d '\n' | sed 's/\r//g') + + # Check if response contains fault + if echo "$flat_response" | grep -q "SOAP-ENV:Fault"; then + # Extract fault string for error reporting + echo "$flat_response" | sed -n 's/.*\(.*\)<\/faultstring>.*/\1/p' | sed 's/ //g' + return 1 + fi + + # Extract successful result + echo "$flat_response" | sed -n 's/.*\(.*\)<\/result>.*/\1/p' | sed 's/ //g' + return 0 +} + +# Generate Base32 TOTP secret +generate_totp_secret() { + # Use existing generation logic from generate-2fa-qr.sh + if command -v base32 >/dev/null 2>&1; then + openssl rand 10 | base32 -w0 | head -c16 + else + # Fallback using Python + python3 -c " +import base64 +import os +secret_bytes = os.urandom(10) +secret_b32 = base64.b32encode(secret_bytes).decode('ascii').rstrip('=') +print(secret_b32[:16]) +" + fi +} + +# Validate Base32 secret format +validate_base32_secret() { + local secret="$1" + if [[ ! "$secret" =~ ^[A-Z2-7]+$ ]]; then + return 1 + fi + if [ ${#secret} -ne 16 ]; then + err "AzerothCore SOAP requires a 16-character Base32 secret (got ${#secret})" + return 1 + fi + return 0 +} + +# ============================================================================= +# ACCOUNT DISCOVERY FUNCTIONS +# ============================================================================= + +# Get all accounts that need 2FA setup +get_accounts_needing_2fa() { + local force="$1" + local query + + if [ "$force" = "true" ]; then + # Include accounts that already have 2FA when force is enabled + query="SELECT username FROM account + WHERE username NOT LIKE 'rndbot%' + AND username NOT LIKE 'playerbot%' + ORDER BY username;" + else + # Only accounts without 2FA + query="SELECT username FROM account + WHERE (totp_secret IS NULL OR totp_secret = '') + AND username NOT LIKE 'rndbot%' + AND username NOT LIKE 'playerbot%' + ORDER BY username;" + fi + + mysql_query "$query" | tail -n +2 # Remove header row +} + +# Check if specific account exists +account_exists() { + local username="$1" + local result + + result=$(mysql_query "SELECT COUNT(*) FROM account WHERE username = '$username';" | tail -n +2) + [ "$result" -eq 1 ] +} + +# Check if account already has 2FA +account_has_2fa() { + local username="$1" + local result + + result=$(mysql_query "SELECT COUNT(*) FROM account WHERE username = '$username' AND totp_secret IS NOT NULL AND totp_secret != '';" | tail -n +2) + [ "$result" -eq 1 ] +} + +# ============================================================================= +# 2FA SETUP FUNCTIONS +# ============================================================================= + +# Generate and set up 2FA for a single account +setup_2fa_for_account() { + local username="$1" + local force="$2" + local secret="" + local qr_output="" + + info "Processing account: $username" + + # Check if account exists + if ! account_exists "$username"; then + err "Account '$username' does not exist, skipping" + return 1 + fi + + # Check if account already has 2FA + if account_has_2fa "$username" && [ "$force" != "true" ]; then + warn "Account '$username' already has 2FA configured, use --force to regenerate" + return 0 + fi + + # Generate TOTP secret + secret=$(generate_totp_secret) + if [ -z "$secret" ] || ! validate_base32_secret "$secret"; then + err "Failed to generate valid TOTP secret for $username" + return 1 + fi + + if [ "$OPT_DRY_RUN" = "true" ]; then + log "DRY RUN: Would set 2FA secret for $username: $secret" + return 0 + fi + + # Set 2FA using official AzerothCore SOAP API + local soap_result + if ! soap_result=$(soap_execute_command ".account set 2fa $username $secret"); then + err "Failed to set 2FA for $username via SOAP API: $soap_result" + return 1 + fi + + # Verify success message + if ! echo "$soap_result" | grep -q "Successfully enabled two-factor authentication"; then + err "Unexpected SOAP response for $username: $soap_result" + return 1 + fi + + # Generate QR code if format is 'qr' + if [ "$OPT_FORMAT" = "qr" ]; then + qr_output="$QR_CODES_DIR/${username}_2fa_qr.png" + + if ! "$SCRIPT_DIR/generate-2fa-qr.sh" -u "$username" -s "$secret" -i "$OPT_ISSUER" -o "$qr_output" >/dev/null; then + warn "Failed to generate QR code for $username, but secret was saved" + fi + fi + + # Log setup information + echo "$username,$secret,$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> "$SECRETS_BACKUP" + echo "account set 2fa $username $secret" >> "$CONSOLE_COMMANDS" + + ok "2FA configured for account: $username" + return 0 +} + +# ============================================================================= +# OUTPUT AND REPORTING FUNCTIONS +# ============================================================================= + +# Create output directory structure +create_output_structure() { + local timestamp + timestamp=$(date +"%Y%m%d%H%M%S") + + if [ -n "$OPT_OUTPUT_DIR" ]; then + OUTPUT_BASE_DIR="$OPT_OUTPUT_DIR" + else + OUTPUT_BASE_DIR="$PROJECT_ROOT/2fa-setup-$timestamp" + fi + + # Create directories + mkdir -p "$OUTPUT_BASE_DIR" + QR_CODES_DIR="$OUTPUT_BASE_DIR/qr-codes" + mkdir -p "$QR_CODES_DIR" + + # Set up output files + SETUP_REPORT="$OUTPUT_BASE_DIR/setup-report.txt" + CONSOLE_COMMANDS="$OUTPUT_BASE_DIR/console-commands.txt" + SECRETS_BACKUP="$OUTPUT_BASE_DIR/secrets-backup.csv" + + # Initialize files + echo "# AzerothCore 2FA Console Commands" > "$CONSOLE_COMMANDS" + echo "# Generated on $(date)" >> "$CONSOLE_COMMANDS" + echo "" >> "$CONSOLE_COMMANDS" + + echo "username,secret,generated_date" > "$SECRETS_BACKUP" + + info "Output directory: $OUTPUT_BASE_DIR" +} + +# Generate final setup report +generate_setup_report() { + local total_processed="$1" + local successful="$2" + local failed="$3" + + { + echo "AzerothCore Bulk 2FA Setup Report" + echo "=================================" + echo "" + echo "Generated: $(date)" + echo "Command: $0 $*" + echo "" + echo "Summary:" + echo "--------" + echo "Total accounts processed: $total_processed" + echo "Successfully configured: $successful" + echo "Failed: $failed" + echo "" + echo "Output Files:" + echo "-------------" + echo "- QR Codes: $QR_CODES_DIR/" + echo "- Console Commands: $CONSOLE_COMMANDS" + echo "- Secrets Backup: $SECRETS_BACKUP" + echo "" + echo "Next Steps:" + echo "-----------" + echo "1. Distribute QR codes to users securely" + echo "2. Users scan QR codes with authenticator apps" + echo "3. Verify setup using console commands if needed" + echo "4. Store secrets backup securely and delete when no longer needed" + echo "" + echo "Security Notes:" + echo "--------------" + echo "- QR codes contain sensitive TOTP secrets" + echo "- Secrets backup file contains plaintext secrets" + echo "- Delete or encrypt these files after distribution" + echo "- Secrets are also stored in AzerothCore database" + } > "$SETUP_REPORT" + + info "Setup report generated: $SETUP_REPORT" +} + +# ============================================================================= +# MAIN SCRIPT LOGIC +# ============================================================================= + +# Parse command line arguments +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + --all) + OPT_ALL=true + shift + ;; + --account) + if [ -z "$2" ]; then + fatal "Option --account requires a username argument" + fi + OPT_ACCOUNTS+=("$2") + shift 2 + ;; + --force) + OPT_FORCE=true + shift + ;; + --output-dir) + if [ -z "$2" ]; then + fatal "Option --output-dir requires a path argument" + fi + OPT_OUTPUT_DIR="$2" + shift 2 + ;; + --dry-run) + OPT_DRY_RUN=true + shift + ;; + --issuer) + if [ -z "$2" ]; then + fatal "Option --issuer requires a name argument" + fi + OPT_ISSUER="$2" + shift 2 + ;; + --format) + if [ -z "$2" ]; then + fatal "Option --format requires qr or manual" + fi + if [[ "$2" != "qr" && "$2" != "manual" ]]; then + fatal "Format must be 'qr' or 'manual'" + fi + OPT_FORMAT="$2" + shift 2 + ;; + --soap-user) + if [ -z "$2" ]; then + fatal "Option --soap-user requires a username argument" + fi + SOAP_USERNAME="$2" + shift 2 + ;; + --soap-pass) + if [ -z "$2" ]; then + fatal "Option --soap-pass requires a password argument" + fi + SOAP_PASSWORD="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + fatal "Unknown option: $1" + ;; + esac + done +} + +# Main execution function +main() { + local accounts_to_process=() + local total_processed=0 + local successful=0 + local failed=0 + + # Parse arguments + parse_arguments "$@" + + # Validate options + if [ "$OPT_ALL" = "false" ] && [ ${#OPT_ACCOUNTS[@]} -eq 0 ]; then + fatal "Must specify either --all or --account USERNAME" + fi + + if [ "$OPT_ALL" = "true" ] && [ ${#OPT_ACCOUNTS[@]} -gt 0 ]; then + fatal "Cannot use --all with specific --account options" + fi + + # Load environment variables + MYSQL_PASSWORD=$(read_env "MYSQL_ROOT_PASSWORD" "") + if [ -z "$MYSQL_PASSWORD" ]; then + fatal "MYSQL_ROOT_PASSWORD not found in environment" + fi + + # Initialize SOAP credentials from environment if not provided via CLI + if [ -z "$SOAP_USERNAME" ]; then + SOAP_USERNAME=$(read_env "SOAP_USERNAME" "GM") + fi + if [ -z "$SOAP_PASSWORD" ]; then + SOAP_PASSWORD=$(read_env "SOAP_PASSWORD" "pass") + fi + + # Validate SOAP credentials + if [ -z "$SOAP_USERNAME" ] || [ -z "$SOAP_PASSWORD" ]; then + fatal "SOAP credentials required. Set via --soap-user/--soap-pass or SOAP_USERNAME/SOAP_PASSWORD in .env" + fi + + # Check container health + check_containers + + # Create output structure + create_output_structure + + # Determine accounts to process + if [ "$OPT_ALL" = "true" ]; then + info "Discovering accounts that need 2FA setup..." + readarray -t accounts_to_process < <(get_accounts_needing_2fa "$OPT_FORCE") + + if [ ${#accounts_to_process[@]} -eq 0 ]; then + if [ "$OPT_FORCE" = "true" ]; then + warn "No accounts found in database" + else + ok "All accounts already have 2FA configured" + fi + exit 0 + fi + + info "Found ${#accounts_to_process[@]} accounts to process" + else + accounts_to_process=("${OPT_ACCOUNTS[@]}") + fi + + # Display dry run information + if [ "$OPT_DRY_RUN" = "true" ]; then + warn "DRY RUN MODE - No changes will be made" + info "Would process the following accounts:" + for account in "${accounts_to_process[@]}"; do + echo " - $account" + done + echo "" + fi + + # Process each account + info "Processing ${#accounts_to_process[@]} accounts..." + for account in "${accounts_to_process[@]}"; do + total_processed=$((total_processed + 1)) + + if setup_2fa_for_account "$account" "$OPT_FORCE"; then + successful=$((successful + 1)) + else + failed=$((failed + 1)) + fi + done + + # Generate final report + if [ "$OPT_DRY_RUN" = "false" ]; then + generate_setup_report "$total_processed" "$successful" "$failed" + + # Summary + echo "" + ok "Bulk 2FA setup completed" + info "Processed: $total_processed accounts" + info "Successful: $successful" + info "Failed: $failed" + info "Output directory: $OUTPUT_BASE_DIR" + + if [ "$failed" -gt 0 ]; then + warn "Some accounts failed to process. Check the output for details." + exit 1 + fi + else + info "Dry run completed. Use without --dry-run to execute." + + if [ "$failed" -gt 0 ]; then + warn "Some accounts would fail to process." + exit 1 + fi + fi +} + +# Execute main function with all arguments +main "$@" diff --git a/scripts/bash/generate-2fa-qr.py b/scripts/bash/generate-2fa-qr.py new file mode 100755 index 0000000..2e1974a --- /dev/null +++ b/scripts/bash/generate-2fa-qr.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +AzerothCore 2FA QR Code Generator (Python version) +Generates TOTP secrets and QR codes for AzerothCore accounts +""" + +import argparse +import base64 +import os +import sys +import re + +def validate_base32(secret): + """Validate Base32 secret format""" + if not re.match(r'^[A-Z2-7]+$', secret): + print("Error: Invalid Base32 secret. Only A-Z and 2-7 characters allowed.", file=sys.stderr) + return False + if len(secret) != 16: + print(f"Error: AzerothCore SOAP requires a 16-character Base32 secret (got {len(secret)}).", file=sys.stderr) + return False + return True + +def generate_secret(): + """Generate a random 16-character Base32 secret (AzerothCore SOAP requirement)""" + secret_bytes = os.urandom(10) + secret_b32 = base64.b32encode(secret_bytes).decode('ascii').rstrip('=') + return secret_b32[:16] + +def generate_qr_code(uri, output_path): + """Generate QR code using available library""" + try: + import qrcode + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=6, + border=4, + ) + qr.add_data(uri) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + img.save(output_path) + return True + except ImportError: + print("Error: qrcode library not installed.", file=sys.stderr) + print("Install it with: pip3 install qrcode[pil]", file=sys.stderr) + return False + +def main(): + parser = argparse.ArgumentParser( + description="Generate TOTP secrets and QR codes for AzerothCore 2FA", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s -u john_doe + %(prog)s -u john_doe -o /tmp/qr.png + %(prog)s -u john_doe -s JBSWY3DPEHPK3PXP -i MyServer + """ + ) + + parser.add_argument('-u', '--username', required=True, + help='Target username for 2FA setup') + parser.add_argument('-o', '--output', + help='Path to save QR code image (default: ./USERNAME_2fa_qr.png)') + parser.add_argument('-s', '--secret', + help='Use existing 16-character Base32 secret (generates random if not provided)') + parser.add_argument('-i', '--issuer', default='AzerothCore', + help='Issuer name for the TOTP entry (default: AzerothCore)') + + args = parser.parse_args() + + # Set default output path + if not args.output: + args.output = f"./{args.username}_2fa_qr.png" + + # Generate or validate secret + if args.secret: + print("Using provided secret...") + if not validate_base32(args.secret): + sys.exit(1) + secret = args.secret + else: + print("Generating new TOTP secret...") + secret = generate_secret() + print(f"Generated secret: {secret}") + + # Create TOTP URI + uri = f"otpauth://totp/{args.issuer}:{args.username}?secret={secret}&issuer={args.issuer}" + + # Generate QR code + print("Generating QR code...") + if generate_qr_code(uri, args.output): + print(f"✓ QR code generated successfully: {args.output}") + else: + print("\nManual setup information:") + print(f"Secret: {secret}") + print(f"URI: {uri}") + sys.exit(1) + + # Display setup information + print("\n=== AzerothCore 2FA Setup Information ===") + print(f"Username: {args.username}") + print(f"Secret: {secret}") + print(f"QR Code: {args.output}") + print(f"Issuer: {args.issuer}") + print("\nNext steps:") + print("1. Share the QR code image with the user") + print("2. User scans QR code with authenticator app") + print("3. Run on AzerothCore console:") + print(f" account set 2fa {args.username} {secret}") + print("4. User can now use 6-digit codes for login") + print("\nSecurity Note: Keep the secret secure and delete the QR code after setup.") + +if __name__ == "__main__": + main() diff --git a/scripts/bash/generate-2fa-qr.sh b/scripts/bash/generate-2fa-qr.sh new file mode 100755 index 0000000..f80645a --- /dev/null +++ b/scripts/bash/generate-2fa-qr.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# AzerothCore 2FA QR Code Generator +# Generates TOTP secrets and QR codes for AzerothCore accounts + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to display usage +show_usage() { + echo "Usage: $0 -u USERNAME [-o OUTPUT_PATH] [-s SECRET] [-i ISSUER]" + echo "" + echo "Options:" + echo " -u USERNAME Target username for 2FA setup (required)" + echo " -o OUTPUT_PATH Path to save QR code image (default: ./USERNAME_2fa_qr.png)" + echo " -s SECRET Use existing 16-character Base32 secret (generates random if not provided)" + echo " -i ISSUER Issuer name for the TOTP entry (default: AzerothCore)" + echo " -h Show this help message" + echo "" + echo "Examples:" + echo " $0 -u john_doe" + echo " $0 -u john_doe -o /tmp/qr.png" + echo " $0 -u john_doe -s JBSWY3DPEHPK3PXP -i MyServer" +} + +# Function to validate Base32 +validate_base32() { + local secret="$1" + if [[ ! "$secret" =~ ^[A-Z2-7]+$ ]]; then + echo -e "${RED}Error: Invalid Base32 secret. Only A-Z and 2-7 characters allowed.${NC}" >&2 + return 1 + fi + if [ ${#secret} -ne 16 ]; then + echo -e "${RED}Error: AzerothCore SOAP requires a 16-character Base32 secret (got ${#secret}).${NC}" >&2 + return 1 + fi +} + +# Function to generate Base32 secret +generate_secret() { + # Generate 10 random bytes and encode as 16-character Base32 (AzerothCore SOAP requirement) + if command -v base32 >/dev/null 2>&1; then + openssl rand 10 | base32 -w0 | head -c16 + else + # Fallback using Python if base32 command not available + python3 -c " +import base64 +import os +secret_bytes = os.urandom(10) +secret_b32 = base64.b32encode(secret_bytes).decode('ascii').rstrip('=') +print(secret_b32[:16]) +" + fi +} + +# Default values +USERNAME="" +OUTPUT_PATH="" +SECRET="" +ISSUER="AzerothCore" + +# Parse command line arguments +while getopts "u:o:s:i:h" opt; do + case ${opt} in + u ) + USERNAME="$OPTARG" + ;; + o ) + OUTPUT_PATH="$OPTARG" + ;; + s ) + SECRET="$OPTARG" + ;; + i ) + ISSUER="$OPTARG" + ;; + h ) + show_usage + exit 0 + ;; + \? ) + echo -e "${RED}Invalid option: $OPTARG${NC}" 1>&2 + show_usage + exit 1 + ;; + : ) + echo -e "${RED}Invalid option: $OPTARG requires an argument${NC}" 1>&2 + show_usage + exit 1 + ;; + esac +done + +# Validate required parameters +if [ -z "$USERNAME" ]; then + echo -e "${RED}Error: Username is required.${NC}" >&2 + show_usage + exit 1 +fi + +# Set default output path if not provided +if [ -z "$OUTPUT_PATH" ]; then + OUTPUT_PATH="./${USERNAME}_2fa_qr.png" +fi + +# Generate secret if not provided +if [ -z "$SECRET" ]; then + echo -e "${BLUE}Generating new TOTP secret...${NC}" + SECRET=$(generate_secret) + if [ -z "$SECRET" ]; then + echo -e "${RED}Error: Failed to generate secret.${NC}" >&2 + exit 1 + fi + echo -e "${GREEN}Generated secret: $SECRET${NC}" +else + echo -e "${BLUE}Using provided secret...${NC}" + if ! validate_base32 "$SECRET"; then + exit 1 + fi +fi + +# Create TOTP URI +URI="otpauth://totp/${ISSUER}:${USERNAME}?secret=${SECRET}&issuer=${ISSUER}" + +# Check if qrencode is available +if ! command -v qrencode >/dev/null 2>&1; then + echo -e "${RED}Error: qrencode is not installed.${NC}" >&2 + echo "Install it with: sudo apt-get install qrencode (Ubuntu/Debian) or brew install qrencode (macOS)" + echo "" + echo -e "${BLUE}Manual setup information:${NC}" + echo "Secret: $SECRET" + echo "URI: $URI" + exit 1 +fi + +# Generate QR code +echo -e "${BLUE}Generating QR code...${NC}" +if echo "$URI" | qrencode -s 6 -o "$OUTPUT_PATH"; then + echo -e "${GREEN}✓ QR code generated successfully: $OUTPUT_PATH${NC}" +else + echo -e "${RED}Error: Failed to generate QR code.${NC}" >&2 + exit 1 +fi + +# Display setup information +echo "" +echo -e "${YELLOW}=== AzerothCore 2FA Setup Information ===${NC}" +echo "Username: $USERNAME" +echo "Secret: $SECRET" +echo "QR Code: $OUTPUT_PATH" +echo "Issuer: $ISSUER" +echo "" +echo -e "${BLUE}Next steps:${NC}" +echo "1. Share the QR code image with the user" +echo "2. User scans QR code with authenticator app" +echo "3. Run on AzerothCore console:" +echo -e " ${GREEN}account set 2fa $USERNAME $SECRET${NC}" +echo "4. User can now use 6-digit codes for login" +echo "" +echo -e "${YELLOW}Security Note: Keep the secret secure and delete the QR code after setup.${NC}" diff --git a/scripts/bash/import-pdumps.sh b/scripts/bash/import-pdumps.sh new file mode 100755 index 0000000..822c3e7 --- /dev/null +++ b/scripts/bash/import-pdumps.sh @@ -0,0 +1,283 @@ +#!/bin/bash +# Process and import character pdump files from import/pdumps/ directory +set -euo pipefail + +INVOCATION_DIR="$PWD" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/../.." # Go to project root + +COLOR_RED='\033[0;31m' +COLOR_GREEN='\033[0;32m' +COLOR_YELLOW='\033[1;33m' +COLOR_BLUE='\033[0;34m' +COLOR_RESET='\033[0m' + +log(){ printf '%b\n' "${COLOR_GREEN}$*${COLOR_RESET}"; } +warn(){ printf '%b\n' "${COLOR_YELLOW}$*${COLOR_RESET}"; } +err(){ printf '%b\n' "${COLOR_RED}$*${COLOR_RESET}"; } +info(){ printf '%b\n' "${COLOR_BLUE}$*${COLOR_RESET}"; } +fatal(){ err "$*"; exit 1; } + +# Source environment variables +if [ -f ".env" ]; then + set -a + source .env + set +a +fi + +IMPORT_DIR="./import/pdumps" +MYSQL_PW="${MYSQL_ROOT_PASSWORD:-}" +AUTH_DB="${ACORE_DB_AUTH_NAME:-acore_auth}" +CHARACTERS_DB="${ACORE_DB_CHARACTERS_NAME:-acore_characters}" +DEFAULT_ACCOUNT="${DEFAULT_IMPORT_ACCOUNT:-}" +INTERACTIVE=${INTERACTIVE:-true} + +usage(){ + cat <<'EOF' +Usage: ./import-pdumps.sh [options] + +Automatically process and import all character pdump files from import/pdumps/ directory. + +Options: + --password PASS MySQL root password (overrides env) + --account ACCOUNT Default account for imports (overrides env) + --auth-db NAME Auth database name (overrides env) + --characters-db NAME Characters database name (overrides env) + --non-interactive Don't prompt for missing information + -h, --help Show this help and exit + +Directory Structure: + import/pdumps/ + ├── character1.pdump # Will be imported with default settings + ├── character2.sql # SQL dump files also supported + └── configs/ # Optional: per-file configuration + ├── character1.conf # account=testuser, name=NewName + └── character2.conf # account=12345, guid=5000 + +Configuration File Format (.conf): + account=target_account_name_or_id + name=new_character_name # Optional: rename character + guid=force_specific_guid # Optional: force GUID + +Environment Variables: + MYSQL_ROOT_PASSWORD # MySQL root password + DEFAULT_IMPORT_ACCOUNT # Default account for imports + ACORE_DB_AUTH_NAME # Auth database name + ACORE_DB_CHARACTERS_NAME # Characters database name + +Examples: + # Import all pdumps with environment settings + ./import-pdumps.sh + + # Import with specific password and account + ./import-pdumps.sh --password mypass --account testuser + +EOF +} + +check_dependencies(){ + if ! docker ps >/dev/null 2>&1; then + fatal "Docker is not running or accessible" + fi + + if ! docker exec ac-mysql mysql --version >/dev/null 2>&1; then + fatal "MySQL container (ac-mysql) is not running or accessible" + fi +} + +parse_config_file(){ + local config_file="$1" + local -A config=() + + if [[ -f "$config_file" ]]; then + while IFS='=' read -r key value; do + # Skip comments and empty lines + [[ "$key" =~ ^[[:space:]]*# ]] && continue + [[ -z "$key" ]] && continue + + # Remove leading/trailing whitespace + key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + config["$key"]="$value" + done < "$config_file" + fi + + # Export as variables for the calling function + export CONFIG_ACCOUNT="${config[account]:-}" + export CONFIG_NAME="${config[name]:-}" + export CONFIG_GUID="${config[guid]:-}" +} + +prompt_for_account(){ + local filename="$1" + if [[ "$INTERACTIVE" != "true" ]]; then + fatal "No account specified for $filename and running in non-interactive mode" + fi + + echo "" + warn "No account specified for: $filename" + echo "Available options:" + echo " 1. Provide account name or ID" + echo " 2. Skip this file" + echo "" + + while true; do + read -p "Enter account name/ID (or 'skip'): " account_input + case "$account_input" in + skip|Skip|SKIP) + return 1 + ;; + "") + warn "Please enter an account name/ID or 'skip'" + continue + ;; + *) + echo "$account_input" + return 0 + ;; + esac + done +} + +process_pdump_file(){ + local pdump_file="$1" + local filename + filename=$(basename "$pdump_file") + local config_file="$IMPORT_DIR/configs/${filename%.*}.conf" + + info "Processing: $filename" + + # Parse configuration file if it exists + parse_config_file "$config_file" + + # Determine account + local target_account="${CONFIG_ACCOUNT:-$DEFAULT_ACCOUNT}" + if [[ -z "$target_account" ]]; then + if ! target_account=$(prompt_for_account "$filename"); then + warn "Skipping $filename (no account provided)" + return 0 + fi + fi + + # Build command arguments + local cmd_args=( + --file "$pdump_file" + --account "$target_account" + --password "$MYSQL_PW" + --auth-db "$AUTH_DB" + --characters-db "$CHARACTERS_DB" + ) + + # Add optional parameters if specified in config + [[ -n "$CONFIG_NAME" ]] && cmd_args+=(--name "$CONFIG_NAME") + [[ -n "$CONFIG_GUID" ]] && cmd_args+=(--guid "$CONFIG_GUID") + + log "Importing $filename to account $target_account" + [[ -n "$CONFIG_NAME" ]] && log " Character name: $CONFIG_NAME" + [[ -n "$CONFIG_GUID" ]] && log " Forced GUID: $CONFIG_GUID" + + # Execute the import + if "./scripts/bash/pdump-import.sh" "${cmd_args[@]}"; then + log "✅ Successfully imported: $filename" + + # Move processed file to processed/ subdirectory + local processed_dir="$IMPORT_DIR/processed" + mkdir -p "$processed_dir" + mv "$pdump_file" "$processed_dir/" + [[ -f "$config_file" ]] && mv "$config_file" "$processed_dir/" + + else + err "❌ Failed to import: $filename" + return 1 + fi +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --password) + [[ $# -ge 2 ]] || fatal "--password requires a value" + MYSQL_PW="$2" + shift 2 + ;; + --account) + [[ $# -ge 2 ]] || fatal "--account requires a value" + DEFAULT_ACCOUNT="$2" + shift 2 + ;; + --auth-db) + [[ $# -ge 2 ]] || fatal "--auth-db requires a value" + AUTH_DB="$2" + shift 2 + ;; + --characters-db) + [[ $# -ge 2 ]] || fatal "--characters-db requires a value" + CHARACTERS_DB="$2" + shift 2 + ;; + --non-interactive) + INTERACTIVE=false + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fatal "Unknown option: $1" + ;; + esac +done + +# Validate required parameters +[[ -n "$MYSQL_PW" ]] || fatal "MySQL password required (use --password or set MYSQL_ROOT_PASSWORD)" + +# Check dependencies +check_dependencies + +# Check if import directory exists and has files +if [[ ! -d "$IMPORT_DIR" ]]; then + info "Import directory doesn't exist: $IMPORT_DIR" + info "Create the directory and place your .pdump or .sql files there." + exit 0 +fi + +# Find pdump files +shopt -s nullglob +pdump_files=("$IMPORT_DIR"/*.pdump "$IMPORT_DIR"/*.sql) +shopt -u nullglob + +if [[ ${#pdump_files[@]} -eq 0 ]]; then + info "No pdump files found in $IMPORT_DIR" + info "Place your .pdump or .sql files in this directory to import them." + exit 0 +fi + +log "Found ${#pdump_files[@]} pdump file(s) to process" + +# Create configs directory if it doesn't exist +mkdir -p "$IMPORT_DIR/configs" + +# Process each file +processed=0 +failed=0 + +for pdump_file in "${pdump_files[@]}"; do + if process_pdump_file "$pdump_file"; then + ((processed++)) + else + ((failed++)) + fi +done + +echo "" +log "Import summary:" +log " ✅ Processed: $processed" +[[ $failed -gt 0 ]] && err " ❌ Failed: $failed" + +if [[ $processed -gt 0 ]]; then + log "" + log "Character imports completed! Processed files moved to $IMPORT_DIR/processed/" + log "You can now log in and access your imported characters." +fi \ No newline at end of file diff --git a/scripts/bash/pdump-import.sh b/scripts/bash/pdump-import.sh new file mode 100755 index 0000000..01c1534 --- /dev/null +++ b/scripts/bash/pdump-import.sh @@ -0,0 +1,344 @@ +#!/bin/bash +# Import character pdump files into AzerothCore database +set -euo pipefail + +INVOCATION_DIR="$PWD" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +COLOR_RED='\033[0;31m' +COLOR_GREEN='\033[0;32m' +COLOR_YELLOW='\033[1;33m' +COLOR_BLUE='\033[0;34m' +COLOR_RESET='\033[0m' + +log(){ printf '%b\n' "${COLOR_GREEN}$*${COLOR_RESET}"; } +warn(){ printf '%b\n' "${COLOR_YELLOW}$*${COLOR_RESET}"; } +err(){ printf '%b\n' "${COLOR_RED}$*${COLOR_RESET}"; } +info(){ printf '%b\n' "${COLOR_BLUE}$*${COLOR_RESET}"; } +fatal(){ err "$*"; exit 1; } + +MYSQL_PW="" +PDUMP_FILE="" +TARGET_ACCOUNT="" +NEW_CHARACTER_NAME="" +FORCE_GUID="" +AUTH_DB="acore_auth" +CHARACTERS_DB="acore_characters" +DRY_RUN=false +BACKUP_BEFORE=true + +usage(){ + cat <<'EOF' +Usage: ./pdump-import.sh [options] + +Import character pdump files into AzerothCore database. + +Required Options: + -f, --file FILE Pdump file to import (.pdump or .sql format) + -a, --account ACCOUNT Target account name or ID for character import + -p, --password PASS MySQL root password + +Optional: + -n, --name NAME New character name (if different from dump) + -g, --guid GUID Force specific character GUID + --auth-db NAME Auth database schema name (default: acore_auth) + --characters-db NAME Characters database schema name (default: acore_characters) + --dry-run Validate pdump without importing + --no-backup Skip pre-import backup (not recommended) + -h, --help Show this help and exit + +Examples: + # Import character from pdump file + ./pdump-import.sh --file character.pdump --account testaccount --password azerothcore123 + + # Import with new character name + ./pdump-import.sh --file oldchar.pdump --account newaccount --name "NewCharName" --password azerothcore123 + + # Validate pdump file without importing + ./pdump-import.sh --file character.pdump --account testaccount --password azerothcore123 --dry-run + +Notes: + - Account must exist in the auth database before import + - Character names must be unique across the server + - Pre-import backup is created automatically (can be disabled with --no-backup) + - Use --dry-run to validate pdump structure before actual import +EOF +} + +validate_account(){ + local account="$1" + if [[ "$account" =~ ^[0-9]+$ ]]; then + # Account ID provided + local count + count=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B -e \ + "SELECT COUNT(*) FROM ${AUTH_DB}.account WHERE id = $account;") + [[ "$count" -eq 1 ]] || fatal "Account ID $account not found in auth database" + else + # Account name provided + local count + count=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B -e \ + "SELECT COUNT(*) FROM ${AUTH_DB}.account WHERE username = '$account';") + [[ "$count" -eq 1 ]] || fatal "Account '$account' not found in auth database" + fi +} + +get_account_id(){ + local account="$1" + if [[ "$account" =~ ^[0-9]+$ ]]; then + echo "$account" + else + docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B -e \ + "SELECT id FROM ${AUTH_DB}.account WHERE username = '$account';" + fi +} + +validate_character_name(){ + local name="$1" + # Check character name format (WoW naming rules) + if [[ ! "$name" =~ ^[A-Za-z]{2,12}$ ]]; then + fatal "Invalid character name: '$name'. Must be 2-12 letters, no numbers or special characters." + fi + + # Check if character name already exists + local count + count=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B -e \ + "SELECT COUNT(*) FROM ${CHARACTERS_DB}.characters WHERE name = '$name';") + [[ "$count" -eq 0 ]] || fatal "Character name '$name' already exists in database" +} + +get_next_guid(){ + docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B -e \ + "SELECT COALESCE(MAX(guid), 0) + 1 FROM ${CHARACTERS_DB}.characters;" +} + +validate_pdump_format(){ + local file="$1" + if [[ ! -f "$file" ]]; then + fatal "Pdump file not found: $file" + fi + + # Check if file is readable and has SQL-like content + if ! head -10 "$file" | grep -q -i "INSERT\|UPDATE\|CREATE\|ALTER"; then + warn "File does not appear to contain SQL statements. Continuing anyway..." + fi + + info "Pdump file validation: OK" +} + +backup_characters(){ + local timestamp + timestamp=$(date +%Y%m%d_%H%M%S) + local backup_file="manual-backups/characters-pre-pdump-import-${timestamp}.sql" + mkdir -p manual-backups + + log "Creating backup: $backup_file" + docker exec ac-mysql mysqldump -uroot -p"$MYSQL_PW" "$CHARACTERS_DB" > "$backup_file" + echo "$backup_file" +} + +process_pdump_sql(){ + local file="$1" + local account_id="$2" + local new_guid="${3:-}" + local new_name="${4:-}" + + # Create temporary processed file + local temp_file + temp_file=$(mktemp) + + # Process the pdump SQL file + # Replace account references and optionally GUID/name + if [[ -n "$new_guid" && -n "$new_name" ]]; then + sed -e "s/\([^0-9]\)[0-9]\+\([^0-9].*account.*=\)/\1${account_id}\2/g" \ + -e "s/\([^0-9]\)[0-9]\+\([^0-9].*guid.*=\)/\1${new_guid}\2/g" \ + -e "s/'[^']*'\([^']*name.*=\)/'${new_name}'\1/g" \ + "$file" > "$temp_file" + elif [[ -n "$new_guid" ]]; then + sed -e "s/\([^0-9]\)[0-9]\+\([^0-9].*account.*=\)/\1${account_id}\2/g" \ + -e "s/\([^0-9]\)[0-9]\+\([^0-9].*guid.*=\)/\1${new_guid}\2/g" \ + "$file" > "$temp_file" + elif [[ -n "$new_name" ]]; then + sed -e "s/\([^0-9]\)[0-9]\+\([^0-9].*account.*=\)/\1${account_id}\2/g" \ + -e "s/'[^']*'\([^']*name.*=\)/'${new_name}'\1/g" \ + "$file" > "$temp_file" + else + sed -e "s/\([^0-9]\)[0-9]\+\([^0-9].*account.*=\)/\1${account_id}\2/g" \ + "$file" > "$temp_file" + fi + + echo "$temp_file" +} + +import_pdump(){ + local processed_file="$1" + + log "Importing character data into $CHARACTERS_DB database" + if docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$CHARACTERS_DB" < "$processed_file"; then + log "Character import completed successfully" + else + fatal "Character import failed. Check MySQL logs for details." + fi +} + +case "${1:-}" in + -h|--help) usage; exit 0;; +esac + +# Parse command line arguments +POSITIONAL=() +while [[ $# -gt 0 ]]; do + case "$1" in + -f|--file) + [[ $# -ge 2 ]] || fatal "--file requires a file path" + PDUMP_FILE="$2" + shift 2 + ;; + -a|--account) + [[ $# -ge 2 ]] || fatal "--account requires an account name or ID" + TARGET_ACCOUNT="$2" + shift 2 + ;; + -p|--password) + [[ $# -ge 2 ]] || fatal "--password requires a value" + MYSQL_PW="$2" + shift 2 + ;; + -n|--name) + [[ $# -ge 2 ]] || fatal "--name requires a character name" + NEW_CHARACTER_NAME="$2" + shift 2 + ;; + -g|--guid) + [[ $# -ge 2 ]] || fatal "--guid requires a GUID number" + FORCE_GUID="$2" + shift 2 + ;; + --auth-db) + [[ $# -ge 2 ]] || fatal "--auth-db requires a value" + AUTH_DB="$2" + shift 2 + ;; + --characters-db) + [[ $# -ge 2 ]] || fatal "--characters-db requires a value" + CHARACTERS_DB="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --no-backup) + BACKUP_BEFORE=false + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + while [[ $# -gt 0 ]]; do + POSITIONAL+=("$1") + shift + done + break + ;; + -*) + fatal "Unknown option: $1" + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac +done + +# Validate required arguments +[[ -n "$PDUMP_FILE" ]] || fatal "Pdump file is required. Use --file FILE" +[[ -n "$TARGET_ACCOUNT" ]] || fatal "Target account is required. Use --account ACCOUNT" +[[ -n "$MYSQL_PW" ]] || fatal "MySQL password is required. Use --password PASS" + +# Resolve relative paths +if [[ ! "$PDUMP_FILE" =~ ^/ ]]; then + PDUMP_FILE="$INVOCATION_DIR/$PDUMP_FILE" +fi + +# Validate inputs +log "Validating pdump file..." +validate_pdump_format "$PDUMP_FILE" + +log "Validating target account..." +validate_account "$TARGET_ACCOUNT" +ACCOUNT_ID=$(get_account_id "$TARGET_ACCOUNT") +log "Target account ID: $ACCOUNT_ID" + +if [[ -n "$NEW_CHARACTER_NAME" ]]; then + log "Validating new character name..." + validate_character_name "$NEW_CHARACTER_NAME" +fi + +# Determine GUID +if [[ -n "$FORCE_GUID" ]]; then + CHARACTER_GUID="$FORCE_GUID" + log "Using forced GUID: $CHARACTER_GUID" +else + CHARACTER_GUID=$(get_next_guid) + log "Using next available GUID: $CHARACTER_GUID" +fi + +# Process pdump file +log "Processing pdump file..." +PROCESSED_FILE=$(process_pdump_sql "$PDUMP_FILE" "$ACCOUNT_ID" "$CHARACTER_GUID" "$NEW_CHARACTER_NAME") + +if $DRY_RUN; then + info "DRY RUN: Pdump processing completed successfully" + info "Processed file saved to: $PROCESSED_FILE" + info "Account ID: $ACCOUNT_ID" + info "Character GUID: $CHARACTER_GUID" + [[ -n "$NEW_CHARACTER_NAME" ]] && info "Character name: $NEW_CHARACTER_NAME" + info "Run without --dry-run to perform actual import" + rm -f "$PROCESSED_FILE" + exit 0 +fi + +# Create backup before import +BACKUP_FILE="" +if $BACKUP_BEFORE; then + BACKUP_FILE=$(backup_characters) +fi + +# Stop world server to prevent issues during import +log "Stopping world server for safe import..." +docker stop ac-worldserver >/dev/null 2>&1 || warn "World server was not running" + +# Perform import +trap 'rm -f "$PROCESSED_FILE"' EXIT +import_pdump "$PROCESSED_FILE" + +# Restart world server +log "Restarting world server..." +docker start ac-worldserver >/dev/null 2>&1 + +# Wait for server to initialize +log "Waiting for world server to initialize..." +for i in {1..30}; do + if docker exec ac-worldserver pgrep worldserver >/dev/null 2>&1; then + log "World server is running" + break + fi + if [ $i -eq 30 ]; then + warn "World server took longer than expected to start" + fi + sleep 2 +done + +# Verify import +CHARACTER_COUNT=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B -e \ + "SELECT COUNT(*) FROM ${CHARACTERS_DB}.characters WHERE account = $ACCOUNT_ID;") + +log "Import completed successfully!" +log "Characters on account $TARGET_ACCOUNT: $CHARACTER_COUNT" +[[ -n "$BACKUP_FILE" ]] && log "Backup created: $BACKUP_FILE" + +info "Character import from pdump completed. You can now log in and play!" \ No newline at end of file diff --git a/scripts/bash/test-2fa-token.py b/scripts/bash/test-2fa-token.py new file mode 100755 index 0000000..b6f0393 --- /dev/null +++ b/scripts/bash/test-2fa-token.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Test TOTP token generation for AzerothCore 2FA +""" + +import base64 +import hmac +import hashlib +import struct +import time +import argparse + +def generate_totp(secret, timestamp=None, interval=30): + """Generate TOTP token from Base32 secret""" + if timestamp is None: + timestamp = int(time.time()) + + # Calculate time counter + counter = timestamp // interval + + # Decode Base32 secret + # Add padding if needed + secret = secret.upper() + missing_padding = len(secret) % 8 + if missing_padding: + secret += '=' * (8 - missing_padding) + + key = base64.b32decode(secret) + + # Pack counter as big-endian 8-byte integer + counter_bytes = struct.pack('>Q', counter) + + # Generate HMAC-SHA1 hash + hmac_hash = hmac.new(key, counter_bytes, hashlib.sha1).digest() + + # Dynamic truncation + offset = hmac_hash[-1] & 0xf + code = struct.unpack('>I', hmac_hash[offset:offset + 4])[0] + code &= 0x7fffffff + code %= 1000000 + + return f"{code:06d}" + +def main(): + parser = argparse.ArgumentParser(description="Generate TOTP tokens for testing") + parser.add_argument('-s', '--secret', required=True, help='Base32 secret') + parser.add_argument('-t', '--time', type=int, help='Unix timestamp (default: current time)') + parser.add_argument('-c', '--count', type=int, default=1, help='Number of tokens to generate') + + args = parser.parse_args() + + timestamp = args.time or int(time.time()) + + print(f"Secret: {args.secret}") + print(f"Timestamp: {timestamp} ({time.ctime(timestamp)})") + print(f"Interval: 30 seconds") + print() + + for i in range(args.count): + current_time = timestamp + (i * 30) + token = generate_totp(args.secret, current_time) + print(f"Time: {time.ctime(current_time)} | Token: {token}") + +if __name__ == "__main__": + main() \ No newline at end of file