diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5c4751c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +MINECRAFT_SERVER_IP= +MINECRAFT_SERVER_RCON_PORT= +MINECRAFT_SERVER_QUERY_PORT= \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4f2431c..6bb9ab4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,11 +1,52 @@ name: PR on: pull_request: - types: [opened] + types: [opened, synchronize] jobs: + build: + runs-on: ubuntu-latest + outputs: + pylint-result: ${{ steps.pylint.outputs.result }} + steps: + - uses: actions/checkout@v4 + + - name: Setup python env + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install -r requirements.txt + + - name: Run pylint + id: pylint + run: | + set +e + OUTPUT=$(pylint $(git ls-files '*.py') 2>&1) + EXIT_CODE=$? + set -e + + echo "Pylint Output" + echo "$OUTPUT" + + echo "result<> $GITHUB_OUTPUT + if [ -z "$OUTPUT" ] || [ $EXIT_CODE -eq 0 ]; then + echo "No errors found" >> $GITHUB_OUTPUT + else + echo "$OUTPUT" | tail -20 >> $GITHUB_OUTPUT + fi + echo "EOF" >> $GITHUB_OUTPUT + + exit $EXIT_CODE + SendDiscordWebhook: runs-on: ubuntu-latest + needs: build + if: always() steps: - name: Discord notification env: @@ -13,12 +54,13 @@ jobs: uses: Ilshidur/action-discord@master with: args: | - **RCON PR** + **PR** - **${{ github.event.pull_request.title }}** ${{ github.actor }} has raised a pull request! - - **${{ github.event.pull_request.title }}** - ${{ github.event.pull_request.body }} - + + Pylint result: + ``` + ${{ needs.build.outputs.pylint-result }} + ``` ${{ github.event.pull_request.html_url }} - + <@987695835678662666> \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad82eda --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# IDE/Env related +.idea/ +.venv/ +venv/ + +.DS_Store +.env + +# Flask related +*.pyc +__pycache__/ + +instance/ + +.pytest_cache/ +.coverage +htmlcov/ + +# Later stuff +terraria.md diff --git a/README.md b/README.md index 171dba1..914cea0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ # RCON A remote console flask web-app for minecraft servers + +### Development +To run the development server enter `flask --app hello run` from the project root after installing all dependencies in `requirements.txt` by using `pip install -r requirements.txt` + +When developing on this repo you'll need to create a `.env` file, there's a `.env.example` file that should look something like this: +``` +MINECRAFT_SERVER_IP= +MINECRAFT_SERVER_RCON_PORT= +MINECRAFT_SERVER_QUERY_PORT= +``` + +Mine (when populated) looks like this: +``` +MINECRAFT_SERVER_IP=10.0.0.150 +MINECRAFT_SERVER_RCON_PORT=25566 +MINECRAFT_SERVER_QUERY_PORT=25567 +``` +When setting these it's important to know the RCON AND QUERY ports of your minecraft server, and *not* the general access port for the server. By default minecraft (java edition), uses port 25565 for the game, so if you're using that, you've got the wrong port. You can find the rcon and query ports in the `server.properties` file under `rcon.port` and `query.port` variables. + +To use the file simply copy the `.env.example` and rename it as `.env` and populate it with your server's details. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..7450302 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,22 @@ +""" +https://flask.palletsprojects.com/en/stable/patterns/appfactories/ +https://www.geeksforgeeks.org/python/__init__-in-python/ +https://medium.com/@ferrohardian/application-factory-pattern-starting-your-flask-project- +""" +# This __init__ file is an application factory + +import os +from flask import Flask +from flask.cli import load_dotenv + +def create_app(): + """ + Main create app function, used as a constructor + """ + app = Flask(__name__) + load_dotenv(".env") + + from . import routes # pylint: disable=import-outside-toplevel + app.register_blueprint(routes.bp) + + return app diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..494c67f --- /dev/null +++ b/app/routes.py @@ -0,0 +1,16 @@ +""" +References: +https://flask.palletsprojects.com/en/stable/tutorial/views/ +https://www.geeksforgeeks.org/python/flask-blueprints/ +""" + +from flask import Blueprint + +bp = Blueprint('blueprint', __name__) + +@bp.route('/hello') +def hello(): + """ + First route as a guideline + """ + return "Hello world" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..108248c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +flask-socketio +python-dotenv +pylint \ No newline at end of file diff --git a/task-design.md b/task-design.md new file mode 100644 index 0000000..15d1bb1 --- /dev/null +++ b/task-design.md @@ -0,0 +1,369 @@ +# Minecraft Remote Console — MVP Design Document + +## Overview + +A lightweight web application providing real-time remote console access to two Minecraft servers via RCON. Users authenticate with a shared password, connect to a server, send commands, and see responses in a terminal-style interface. + +--- + +## Architectural Constraints + +| Constraint | Decision | +|------------|----------| +| Deployment | Same host as Minecraft servers | +| Console data source | RCON only (no log file tailing) | +| User sessions | Isolated (each user sees only their own commands) | +| Command filtering | None (all commands permitted) | +| Session history | Ephemeral (fresh console on each connection) | +| Connection errors | Display error, disable input until resolved | + +--- + +## Tech Stack + +### Backend + +| Component | Technology | Rationale | +|-----------|------------|-----------| +| Web framework | Flask 3.x | Lightweight, beginner-friendly, excellent documentation | +| Real-time transport | Flask-SocketIO 5.x | Abstracts WebSocket complexity, handles reconnection, integrates cleanly with Flask | +| Async worker | gevent or eventlet | Required by Flask-SocketIO for WebSocket support | +| RCON client | mcrcon | Simple, well-maintained Python RCON library | +| Configuration | python-dotenv | Load environment variables from .env file | +| Password hashing | Werkzeug (built into Flask) | Secure password verification without extra dependencies | + +### Frontend + +| Component | Technology | Rationale | +|-----------|------------|-----------| +| Templating | Jinja2 (Flask built-in) | Server-rendered HTML, no build step required | +| Real-time client | Socket.IO JS client (CDN) | Matches Flask-SocketIO, handles reconnection automatically | +| Styling | Vanilla CSS | No framework needed for terminal aesthetic | +| JavaScript | Vanilla ES6 | No build step, easy for beginners to understand | + +### Infrastructure + +| Component | Technology | Rationale | +|-----------|------------|-----------| +| Process manager | systemd | Already on Linux hosts, production-ready | +| WSGI server | gunicorn with gevent worker | Required for Socket.IO in production | +| Reverse proxy | nginx (optional) | TLS termination if exposed beyond localhost | + +--- + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Browser │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ Single Page Application │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ [Vanilla ▼] [Connected] │ │ │ +│ │ ├─────────────────────────────────────────────────────────┤ │ │ +│ │ │ │ │ │ +│ │ │ > /list │ │ │ +│ │ │ There are 3/20 players online: Alex, Steve, Notch │ │ │ +│ │ │ > /time set day │ │ │ +│ │ │ Set the time to 1000 │ │ │ +│ │ │ > _ │ │ │ +│ │ │ │ │ │ +│ │ ├─────────────────────────────────────────────────────────┤ │ │ +│ │ │ [____________________________________] [Send] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ Socket.IO (WebSocket + HTTP fallback) + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Flask Application │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │ +│ │ Routes │ │ SocketIO │ │ RCON Manager │ │ +│ │ │ │ Handlers │ │ │ │ +│ │ GET / │ │ │ │ ┌───────────────────────┐ │ │ +│ │ GET /login │ │ connect │ │ │ Vanilla Connection │ │ │ +│ │ POST /login │◄─┤ disconnect │◄─┤ │ - health check │ │ │ +│ │ POST /logout│ │ command │ │ │ - execute(cmd) → resp │ │ │ +│ │ │ │ switch │ │ └───────────────────────┘ │ │ +│ └─────────────┘ └─────────────┘ │ ┌───────────────────────┐ │ │ +│ │ │ Modded Connection │ │ │ +│ │ │ - health check │ │ │ +│ │ │ - execute(cmd) → resp │ │ │ +│ │ └───────────────────────┘ │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Configuration │ │ +│ │ Loaded from .env: server details, password hash, secrets │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ┌──────────────┴──────────────┐ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ Vanilla Server │ │ Modded Server │ + │ │ │ │ + │ RCON: 25575 │ │ RCON: 25576 │ + └─────────────────┘ └─────────────────┘ +``` + +--- + +## Component Design + +### 1. Flask Routes (HTTP) + +| Route | Method | Auth Required | Purpose | +|-------|--------|---------------|---------| +| `/` | GET | Yes | Serve main console page | +| `/login` | GET | No | Serve login form | +| `/login` | POST | No | Validate password, create session | +| `/logout` | POST | Yes | Clear session, redirect to login | +| `/health` | GET | No | Application health check | + +### 2. Socket.IO Events + +**Client → Server** + +| Event | Payload | Description | +|-------|---------|-------------| +| `connect` | (automatic) | Client establishes WebSocket connection | +| `switch_server` | `{server_id: "vanilla"}` | Client wants to use a different server | +| `command` | `{command: "/list"}` | Client sends command to current server | + +**Server → Client** + +| Event | Payload | Description | +|-------|---------|-------------| +| `connected` | `{server_id: "vanilla", server_name: "Vanilla SMP", status: "ok"}` | Confirms connection, provides initial state | +| `server_status` | `{server_id: "vanilla", status: "ok" \| "error", message?: "..."}` | Server health update after switch or error | +| `command_echo` | `{command: "/list"}` | Echo back the command (for display) | +| `command_response` | `{response: "There are 3/20 players..."}` | RCON response | +| `error` | `{message: "RCON connection failed", recoverable: true}` | Error notification | + +### 3. RCON Manager + +Responsibilities: +- Maintain RCON connection state per server +- Execute commands and return responses +- Provide health check capability +- Handle connection failures gracefully + +Behaviour: +- Connections are established lazily (on first command or health check) +- Failed connections are retried on next command attempt +- Each command opens a fresh RCON connection (simpler than pooling for MVP) + +### 4. Session State + +Each WebSocket connection (Socket.IO session) tracks: + +```python +{ + "user_authenticated": True, + "current_server": "vanilla" # or "modded" +} +``` + +No database — state lives in memory, tied to the WebSocket connection lifetime. + +--- + +## Data Flow: Sending a Command + +``` +1. User types "/list" and clicks Send + │ + ▼ +2. Browser emits Socket.IO event + Event: "command" + Payload: {command: "/list"} + │ + ▼ +3. Flask-SocketIO handler receives event + - Validates user is authenticated (session check) + - Looks up user's current server from session + - Emits "command_echo" back to client immediately + │ + ▼ +4. RCON Manager executes command + - Opens TCP connection to server's RCON port + - Sends RCON command packet + - Receives response packet + - Closes connection + │ + ▼ +5. Handler emits response to client + Event: "command_response" + Payload: {response: "There are 3/20 players online: Alex, Steve, Notch"} + │ + ▼ +6. Browser appends response to console display +``` + +--- + +## Data Flow: Connection Failure + +``` +1. User sends command (or switches server) + │ + ▼ +2. RCON Manager attempts connection + - TCP connection refused / timeout / auth failure + │ + ▼ +3. Handler catches exception + - Emits "server_status" with status: "error" + - Emits "error" with message details + │ + ▼ +4. Browser receives error + - Displays error banner + - Disables command input + - Shows retry button (triggers health check) +``` + +--- + +## Project Structure + +``` +mc-console/ +├── app/ +│ ├── __init__.py # Flask app factory, SocketIO init +│ ├── config.py # Configuration loading from environment +│ ├── routes.py # HTTP route handlers +│ ├── events.py # Socket.IO event handlers +│ ├── rcon_manager.py # RCON connection and command execution +│ ├── auth.py # Authentication helpers +│ ├── templates/ +│ │ ├── base.html # Base template with common structure +│ │ ├── login.html # Login page +│ │ └── console.html # Main console interface +│ └── static/ +│ ├── css/ +│ │ └── terminal.css # Terminal styling +│ └── js/ +│ └── console.js # Socket.IO client logic +├── .env # Environment configuration (git-ignored) +├── .env.example # Example configuration (committed) +├── requirements.txt # Python dependencies +├── run.py # Development server entry point +└── README.md # Setup and usage instructions +``` + +--- + +## Configuration Schema + +```env +# .env + +# Flask +SECRET_KEY=your-random-secret-key-here + +# Authentication +CONSOLE_PASSWORD_HASH=pbkdf2:sha256:600000$... + +# Vanilla Server +VANILLA_NAME=Vanilla SMP +VANILLA_RCON_HOST=127.0.0.1 +VANILLA_RCON_PORT=25575 +VANILLA_RCON_PASSWORD=your-vanilla-rcon-password + +# Modded Server +MODDED_NAME=Modded ATM9 +MODDED_RCON_HOST=127.0.0.1 +MODDED_RCON_PORT=25576 +MODDED_RCON_PASSWORD=your-modded-rcon-password +``` + +--- + +## Security Considerations + +| Concern | Mitigation | +|---------|------------| +| Password storage | Stored as hash using Werkzeug's PBKDF2 | +| Session hijacking | Flask signed cookies, SECRET_KEY must be strong | +| RCON password exposure | Never sent to frontend, only used server-side | +| XSS in console output | Escape all output before rendering in browser | +| CSRF | Socket.IO connections validated against session | +| Network exposure | Bind to 127.0.0.1 by default; use nginx + TLS if exposing | + +--- + +## Dependencies + +``` +# requirements.txt + +flask>=3.0.0 +flask-socketio>=5.3.0 +python-dotenv>=1.0.0 +mcrcon>=0.7.0 +gevent>=24.0.0 +gevent-websocket>=0.10.1 +``` + +--- + +## Development vs Production + +| Aspect | Development | Production | +|--------|-------------|------------| +| Server | `flask run` or `python run.py` | gunicorn with gevent worker | +| Binding | `127.0.0.1:5000` | Unix socket or `127.0.0.1:5000` behind nginx | +| Debug mode | Enabled | Disabled | +| TLS | None | nginx handles termination | +| Process management | Manual | systemd service | + +### Production Command + +```bash +gunicorn --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \ + --workers 1 \ + --bind 127.0.0.1:5000 \ + "app:create_app()" +``` + +--- + +## Future Enhancements (Post-MVP) + +| Feature | Complexity | Value | +|---------|------------|-------| +| Log file tailing | Medium | Full console visibility | +| Shared view mode | Low | Team collaboration | +| Command history (in-memory) | Low | Better UX | +| User accounts + roles | Medium | Access control | +| Command audit logging | Medium | Accountability | +| Go RCON service | High | Learning opportunity, better concurrency | +| Multiple server support (dynamic) | Medium | Scalability | +| Container deployment | Medium | Portability | + +--- + +## Task Breakdown for Team + +| Task | Estimated Effort | Dependencies | +|------|------------------|--------------| +| Project scaffolding + config loading | 1-2 hours | None | +| RCON manager module | 2-3 hours | None | +| HTTP routes (login, logout, index) | 2-3 hours | Scaffolding | +| Socket.IO event handlers | 3-4 hours | RCON manager | +| Login page template | 1-2 hours | Routes | +| Console page template + CSS | 3-4 hours | None | +| Console JavaScript (Socket.IO client) | 3-4 hours | Events, template | +| Integration testing | 2-3 hours | All above | +| Documentation + setup guide | 1-2 hours | All above | + +Parallelisation options: +- RCON manager can be built independently +- Templates and CSS can be built without backend +- Integration happens once pieces are ready \ No newline at end of file