Base code
This commit is contained in:
parent
1a89427b06
commit
fb5d68025c
3 changed files with 285 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -102,3 +102,6 @@ venv.bak/
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
# MinecraftServerAPI config
|
||||||
|
config.json
|
||||||
|
|
269
app.py
Executable file
269
app.py
Executable file
|
@ -0,0 +1,269 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from os import urandom
|
||||||
|
from os.path import isfile
|
||||||
|
from pathlib import Path
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from flask import Flask, abort, jsonify, request
|
||||||
|
from flask_jwt import JWT, jwt_required
|
||||||
|
from mcrcon import MCRcon
|
||||||
|
from mcstatus import MinecraftServer
|
||||||
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
# Create default configuration file
|
||||||
|
if not isfile("config.json"):
|
||||||
|
logging.info("No config file, creating nwe one")
|
||||||
|
conf = {
|
||||||
|
"Key": str(urandom(24)),
|
||||||
|
"Users": {"admin": generate_password_hash("admin")},
|
||||||
|
"Path": "",
|
||||||
|
"Jar server": "server.jar",
|
||||||
|
"Server min ram": "1024M",
|
||||||
|
"Server max ram": "1024M",
|
||||||
|
"Server ip": "127.0.0.1",
|
||||||
|
"Rcon port": 25575,
|
||||||
|
"Rcon passwd": "admin",
|
||||||
|
"Query port": 25565,
|
||||||
|
"Properties": {}
|
||||||
|
}
|
||||||
|
with open("config.json", "w") as conf_file:
|
||||||
|
json.dump(conf, conf_file)
|
||||||
|
|
||||||
|
# Load configuration file
|
||||||
|
with open("config.json", "r") as conf_file:
|
||||||
|
logging.info("Loading configurations")
|
||||||
|
conf = json.load(conf_file)
|
||||||
|
|
||||||
|
# Check s server jar exist
|
||||||
|
if not isfile(Path(conf["Path"])/conf["Jar server"]):
|
||||||
|
logging.warning("No server jar found !")
|
||||||
|
exit()
|
||||||
|
# Enable elua by default
|
||||||
|
if not isfile(Path(conf["Path"])/"eula.txt"):
|
||||||
|
logging.info("No elua.txt, creating new one")
|
||||||
|
with open(Path(conf["Path"])/"eula.txt") as elua:
|
||||||
|
elua.write("eula=true")
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration of flask
|
||||||
|
class Config(object):
|
||||||
|
SECRET_KEY = conf["Key"]
|
||||||
|
JWT_AUTH_USERNAME_KEY = "username"
|
||||||
|
|
||||||
|
|
||||||
|
# User object for JWT
|
||||||
|
class User(object):
|
||||||
|
def __init__(self, id, username, password):
|
||||||
|
self.id = id
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "User(id='%s')" % self.id
|
||||||
|
|
||||||
|
|
||||||
|
# Convert user form config to User object
|
||||||
|
# TODO: Support config change
|
||||||
|
users = list()
|
||||||
|
for i, u in enumerate(conf["Users"]):
|
||||||
|
users.append(User(i+1, u, conf["Users"][u]))
|
||||||
|
username_table = {u.username: u for u in users}
|
||||||
|
userid_table = {u.id: u for u in users}
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(username, password):
|
||||||
|
"""
|
||||||
|
Authentication for JWT
|
||||||
|
:param username: User's username (str)
|
||||||
|
:param password: User's password
|
||||||
|
:return: User's object if correct username and password
|
||||||
|
"""
|
||||||
|
user = username_table.get(username, None)
|
||||||
|
if user and check_password_hash(user.password, password):
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def identity(payload):
|
||||||
|
"""
|
||||||
|
Get identity for JWT
|
||||||
|
:param payload: JWT payload
|
||||||
|
:return: User's object
|
||||||
|
"""
|
||||||
|
user_id = payload["identity"]
|
||||||
|
return userid_table.get(user_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def update_properties():
|
||||||
|
"""
|
||||||
|
Update server.properties with configuration arguments
|
||||||
|
"""
|
||||||
|
if isfile(Path(conf["Path"]) / "server.properties"): # Check if properties file exist
|
||||||
|
logging.info("Check server.properties")
|
||||||
|
# Get the content of the file
|
||||||
|
with open(Path(conf["Path"]) / "server.properties", "r") as properties_file:
|
||||||
|
properties = properties_file.read()
|
||||||
|
# Default configuration
|
||||||
|
properties_conf = {"enable-rcon": "true", "enable-query": "true", "rcon.port": conf["Rcon port"],
|
||||||
|
"query.port": conf["Query port"], "rcon.password": conf["Rcon passwd"]}
|
||||||
|
properties_conf.update(conf["Properties"]) # Add of extra configuration
|
||||||
|
for c in properties_conf: # For every configuration
|
||||||
|
# Get the start, the middle (=) and the end of the configuration
|
||||||
|
s = properties.find(c)
|
||||||
|
m = properties.find("=", s)
|
||||||
|
e = properties.find("\n", m)
|
||||||
|
if properties[s:m] == c and properties[m + 1:e] != properties_conf[c]: # Check if configurations match
|
||||||
|
logging.info(f"Change {c} to {properties_conf[c]}")
|
||||||
|
properties = properties.replace(properties[s:e], f"{c}={properties_conf[c]}") # Update configuration
|
||||||
|
# Save configuration changes
|
||||||
|
with open(Path(conf["Path"]) / "server.properties", "w") as properties_file:
|
||||||
|
properties_file.write(properties)
|
||||||
|
|
||||||
|
|
||||||
|
# Setup of globals variables
|
||||||
|
app = Flask(__name__) # Setup Flask's app
|
||||||
|
app.config.from_object(Config) # Import Flask configuration
|
||||||
|
# TODO: support config change
|
||||||
|
mcr = MCRcon(conf["Server ip"], conf["Rcon passwd"], conf["Rcon port"]) # Setup Rcon
|
||||||
|
mcq = MinecraftServer(conf["Server ip"], conf["Query port"]) # Setup Query
|
||||||
|
jwt = JWT(app, authenticate, identity) # Setup JWT
|
||||||
|
server = None # Init server
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
@jwt_required()
|
||||||
|
def root():
|
||||||
|
"""
|
||||||
|
Show server status
|
||||||
|
:return: Actual status of the server
|
||||||
|
"""
|
||||||
|
if server and server.poll() is None: # Check is server is online
|
||||||
|
try: # In case of connexion errors
|
||||||
|
status = mcq.status()
|
||||||
|
query = mcq.query()
|
||||||
|
return f"The server has {status.players.online} players and replied in {status.latency} ms"\
|
||||||
|
+ "\n Online players: " + ", ".join(query.players.names)
|
||||||
|
except OSError:
|
||||||
|
abort(400, "Server did not respond")
|
||||||
|
else:
|
||||||
|
return "Server is offline"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/start")
|
||||||
|
@jwt_required()
|
||||||
|
def start():
|
||||||
|
"""
|
||||||
|
Start the server
|
||||||
|
:return: Ok if everything is fine, 400 error f server already running
|
||||||
|
"""
|
||||||
|
global server # Get the global value for reallocation
|
||||||
|
if not server or server.poll() is not None: # Check if the server is offline
|
||||||
|
update_properties() # Update server.properties
|
||||||
|
# Start the server
|
||||||
|
server = subprocess.Popen(["java", "-Xms"+conf["Server min ram"], "-Xmx"+conf["Server max ram"], "-jar",
|
||||||
|
conf["Jar server"], "nogui"], stdout=subprocess.PIPE, cwd=Path(conf["Path"]))
|
||||||
|
if not isfile(Path(conf["Path"]) / "server.properties"): # If no server.properties reboot to apply the changes
|
||||||
|
# Wait the creation of the properties fle
|
||||||
|
while not isfile(Path(conf["Path"]) / "server.properties"):
|
||||||
|
sleep(1)
|
||||||
|
kill() # Kill the server because no Rcon connection
|
||||||
|
# Wait for subprocesse full exit
|
||||||
|
while server.poll() is None:
|
||||||
|
sleep(1)
|
||||||
|
start() # Start again the server
|
||||||
|
return "Ok"
|
||||||
|
else:
|
||||||
|
abort(400, "Server is running")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stop")
|
||||||
|
@jwt_required()
|
||||||
|
def stop():
|
||||||
|
"""
|
||||||
|
Stop the server
|
||||||
|
:return: The result of the /stop command or 400 error if server is not running
|
||||||
|
"""
|
||||||
|
if server and server.poll() is None: # Check if server is running
|
||||||
|
return cmd("/stop") # Launch /stop command
|
||||||
|
else:
|
||||||
|
abort(400, "Server is not running")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/kill")
|
||||||
|
@jwt_required()
|
||||||
|
def kill():
|
||||||
|
"""
|
||||||
|
Kill the server
|
||||||
|
:return: Ok or 400 if the server is not running
|
||||||
|
"""
|
||||||
|
if server and server.poll() is None: # Check if the server is running
|
||||||
|
server.kill() # Kill the subprocess
|
||||||
|
return "Ok"
|
||||||
|
else:
|
||||||
|
abort(400, "Server is not running")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/cmd/<cmd>")
|
||||||
|
@jwt_required()
|
||||||
|
def cmd(cmd):
|
||||||
|
"""
|
||||||
|
Execute a command by Rcon on the server
|
||||||
|
:param cmd: The command to execute
|
||||||
|
:return: The result of the command or 400 error if server not running also if Rcon connexion fail
|
||||||
|
"""
|
||||||
|
if server and server.poll() is None: # Check if server is running
|
||||||
|
try: # In case of Rcon connexion fail
|
||||||
|
with mcr: # Open a Rco connexion
|
||||||
|
resp = mcr.command(cmd) # Send the command
|
||||||
|
except (TimeoutError, ConnectionRefusedError):
|
||||||
|
abort(400, "Server did not respond")
|
||||||
|
else:
|
||||||
|
return str(resp)
|
||||||
|
else:
|
||||||
|
abort(400, "Server is not running")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/logs")
|
||||||
|
@jwt_required()
|
||||||
|
def logs():
|
||||||
|
"""
|
||||||
|
Get the server logs
|
||||||
|
:return: Server last logs or 400 error if server is not running
|
||||||
|
"""
|
||||||
|
if server and server.poll() is None: # Check if server is running
|
||||||
|
return open(Path(conf["Path"])/"logs"/"latest.log", "r").read() # Send the content of the server logs
|
||||||
|
else:
|
||||||
|
abort(400, "Server is not running")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/config", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_config():
|
||||||
|
"""
|
||||||
|
Get configuration
|
||||||
|
:return: The Json configuration
|
||||||
|
"""
|
||||||
|
return jsonify(conf)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/config", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_config():
|
||||||
|
"""
|
||||||
|
Update the configuration
|
||||||
|
:return: The updated Json configuration
|
||||||
|
"""
|
||||||
|
for p in request.json: # For every entry on the request
|
||||||
|
if p in conf: # Check if it match wth the configuration
|
||||||
|
conf[p] = request.json[p] # Update the configuration
|
||||||
|
# Save configuration changes
|
||||||
|
with open("config.json", "w") as conf_file:
|
||||||
|
json.dump(conf, conf_file)
|
||||||
|
return jsonify(conf)
|
||||||
|
|
||||||
|
|
||||||
|
# Start of the program
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(ssl_context="adhoc")
|
13
requirement.txt
Normal file
13
requirement.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Click==7.0
|
||||||
|
dnspython==1.15.0
|
||||||
|
dnspython3==1.15.0
|
||||||
|
Flask==1.1.1
|
||||||
|
Flask-JWT==0.3.2
|
||||||
|
itsdangerous==1.1.0
|
||||||
|
Jinja2==2.10.3
|
||||||
|
MarkupSafe==1.1.1
|
||||||
|
mcrcon==0.5.2
|
||||||
|
mcstatus==2.2.1
|
||||||
|
PyJWT==1.4.2
|
||||||
|
six==1.12.0
|
||||||
|
Werkzeug==0.16.0
|
Loading…
Reference in a new issue