Some changes.

This commit is contained in:
2026-05-17 19:16:00 +08:00
parent d1663c96e9
commit 2d751f6f19
4 changed files with 290 additions and 146 deletions
+17 -3
View File
@@ -1,6 +1,7 @@
import argparse
import asyncio
import queue
import ssl
import sys
import threading
import socket
@@ -31,6 +32,9 @@ class ServerJarClient(Application):
# Socket
self.sock = None
# Args
self.args = None
# event
self.kb = KeyBindings()
@@ -281,8 +285,9 @@ class ServerJarClient(Application):
def arguments_parser(self):
parser = argparse.ArgumentParser()
# parser.add_argument("-p", "--port", type=int, help="Port number", required=True)
# parser.add_argument('-host', '--host', type=str, help="Hostname", required=True)
parser.add_argument("-p", "--port", type=int, help="Port number", required=True)
parser.add_argument('-host', '--host', type=str, help="Hostname", required=True)
parser.add_argument('-no-tls', '--no-tls', type="store_true", help="Enable TLS support")
args = parser.parse_args()
@@ -374,8 +379,17 @@ class ServerJarClient(Application):
try:
self._log(f"Connecting to {self.host}:{self.port} ...")
# Create connect
if self.args.no_tls:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((self.host, self.port))
else:
raw = socket.create_connection((self.host, self.port))
context = ssl.create_default_context()
s = context.wrap_socket(raw, server_hostname=self.host)
s.connect((self.host, self.port))
with self.sock_lock:
self.sock = s
@@ -421,7 +435,7 @@ class ServerJarClient(Application):
def startup(self):
self.arguments_parser()
self.args = self.arguments_parser()
self.layout.focus(self.input_area)
asyncio.create_task(self.consume_incoming())
+199 -101
View File
@@ -10,30 +10,78 @@ import socketserver
import logging
import os
import queue
import ssl
import sys
import subprocess
import threading
import time
import datetime
from pathlib import Path
import click
import yaml
from utils.common import download_latest_paper_jar, get_latest_version_minecraft, get_specific_version_paper_builds, \
download_server_jar, download_latest_build_paper_jar
download_server_jar, download_latest_build_paper_jar, get_latest_paper_version
from utils.file_settings import FileSettings
from utils.file_settings import required_list, required_value
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
ROOT_DIR = Path(os.getcwd())
SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml"
VERSION = "1.0"
def exit(message):
click.echo(click.style(message, fg='green'))
sys.exit(0)
@click.group()
def main():
print("ServerJar\n"
"WorkDir: {}".format(ROOT_DIR))
print(f"ServerJar v{VERSION}"
f"\nWorkDir: {ROOT_DIR}")
def load_settings():
s = FileSettings(
SERVER_CONFIG_PATH,
{
"servers": [],
"socketServerHostname": "127.0.0.1",
"socketServerPort": 25560
},
{
"socketServerHostname": required_value("127.0.0.1"),
"socketServerPort": required_value(25560),
"enableTLSSupport": required_value(True),
"socketServerCertfile": required_value("data/server-public.pem"),
"socketServerKeyfile": required_value("data/server-private.pem"),
"servers": required_list(
{
"name": "Unnamed Server",
"version": "unknown",
"description": "",
"args": [],
"workDir": "",
"port": 25565,
"host": "127.0.0.1",
"enable": True
},
use_same_form=True,
)
},
dumps_func=yaml.safe_dump,
load_func=yaml.safe_load,
)
if not s.exists():
s.create()
s.read_from_exist()
return s
@main.command()
@@ -49,7 +97,27 @@ def main():
@click.option("--latest", is_flag=True, help="Download latest Minecraft version (With latest build paper)")
@click.option("--list-builds", is_flag=True, help="List available paper build versions")
@click.option("--filename", default=None, help="Custom SERVER.jar file name")
def create_server(name, mc_version, build, snapshot, latest, list_builds, filename):
@click.option("--extra-args", "-e",
help="Extra java arguments", type=str, default="")
@click.option("--custom-args", "-ce",
help="Custom arguments (command)", type=str, default="")
@click.option("--java-exec-path", "-p", show_default=True,
help="The destination of the java executable", default="java")
@click.option("--x-memory-initial", "-xms", show_default=True,
help="Initial allocation size of the memory for server",
type=str, default="1G")
@click.option("--x-memory-maximum", "-xmx", show_default=True,
help="Maximum allocation size of the memory for server",
type=str, default="4G")
@click.option("--nogui", "-ng",
help="Disable server window",
is_flag=True)
@click.option("--server-host", "-srh",
help="Hostname of the server", required=True)
@click.option("--server-port", "-srp",
help="Port of the server", required=True)
def create_server(name, mc_version, build, snapshot, latest, list_builds, filename, extra_args, java_exec_path,
x_memory_initial, x_memory_maximum, nogui, custom_args, server_port, server_host):
server_dir = Path("servers", name)
if server_dir.exists():
@@ -60,14 +128,16 @@ def create_server(name, mc_version, build, snapshot, latest, list_builds, filena
server_dir.mkdir(parents=True, exist_ok=True)
latest_ver = None
try:
release = True if not snapshot else False
if latest:
click.echo("Fetching latest Mojang release version...")
out = download_latest_paper_jar(server_dir, filename=filename, release=release)
click.echo(f"Done: {out}")
return
latest_ver = get_latest_paper_version(release=release)
builds = get_specific_version_paper_builds(latest_ver)
out = download_server_jar(latest_ver, builds[-1], server_dir)
else:
if mc_version is None:
click.echo("The mc-version is not specified. Fetching latest Minecraft release version...")
mc_version = get_latest_version_minecraft(release=release)
@@ -94,76 +164,10 @@ def create_server(name, mc_version, build, snapshot, latest, list_builds, filena
except Exception as e:
raise click.ClickException(str(e))
def load_settings():
s = FileSettings(
SERVER_CONFIG_PATH,
{
"servers": [],
"socketServerHostname": "127.0.0.1",
"socketServerPort": 25560
},
{
"socketServerHostname": required_value("127.0.0.1"),
"socketServerPort": required_value(25560),
"servers": required_list(
{
"name": "Unnamed Server",
"version": "unknown",
"description": "",
"command": "",
"workDir": "",
"port": 25565,
"host": "127.0.0.1",
"enable": True
},
use_same_form=True,
)
},
dumps_func=yaml.safe_dump,
load_func=yaml.safe_load,
)
if not s.exists():
s.create()
s.read_from_exist()
return s
@main.command()
@click.option("--server-folder-path", "-sf",
help="The destination of the folder", required=True)
@click.option("--server-jar-path", "-sp",
help="The destination of the SERVER.jar", required=True)
@click.option("--socket-server-host", "-srh",
help="Hostname of the socket server", required=True)
@click.option("--socket-server-port", "-srp",
help="Port of the socket server", required=True)
@click.option("--java-exec-path", "-p", show_default=True,
help="The destination of the java executable", default="java")
@click.option("--x-memory-initial", "-xms", show_default=True,
help="Initial allocation size of the memory for server",
type=str, default="1G")
@click.option("--x-memory-maximum", "-xmx", show_default=True,
help="Maximum allocation size of the memory for server",
type=str, default="4G")
@click.option("--nogui", "-ng",
help="Disable server window",
is_flag=True)
@click.option("--extra-args", "-e",
help="Extra java arguments", type=str, default="")
@click.option("--custom-commands", "-cd",
help="Custom run commands", type=str, default="")
def create_bootstrap(server_folder_path, server_jar_path, socket_server_host, socket_server_port,
java_exec_path, x_memory_initial, x_memory_maximum, nogui, extra_args, custom_commands):
settings = load_settings()
print("There's some information you need to fill for server config.")
name = str(input("New server name: "))
version = str(input("Server version: "))
name = str(input("New server name: ")) if name is None else name
desc = str(input("Server description: "))
found_exist = False
@@ -175,33 +179,41 @@ def create_bootstrap(server_folder_path, server_jar_path, socket_server_host, so
result = str(input("WARNING: Found duplicate server name. Would you like to continue? [y/N] "))
if not result.lower() == "y":
exit("User aborted.")
return
extra_args += " nogui" if nogui else ""
cmd = f"{java_exec_path} -Xms{x_memory_initial} -Xmx{x_memory_maximum} -jar {server_jar_path} {extra_args}"
extra_args += "nogui" if nogui else ""
args = [
java_exec_path,
"--Xms{}".format(x_memory_initial),
"--Xmx{}".format(x_memory_maximum),
"-jar",
out.absolute().as_posix(),
extra_args,
]
if custom_commands:
if custom_args:
print("Will use custom commands as replacement.")
cmd = custom_commands
args = custom_args
print(f"Server command: {cmd}")
print(f"Server command: {" ".join(args)}")
with settings.edit() as s:
print("Saving...")
s["servers"].append({
"name": name,
"version": version,
"version": latest_ver if latest_ver is not None else mc_version,
"description": desc,
"command": cmd,
"workDir": server_folder_path,
"port": socket_server_port,
"host": socket_server_host,
"args": args,
"workDir": server_dir.absolute().as_posix(),
"port": server_port,
"host": server_host,
"enable": True,
})
print("Done")
class SocketServer:
def __init__(self, host, port):
def __init__(self, host, port, enable_tls, certfile: Path, keyfile: Path):
self.logger = logging.getLogger("SocketServer")
self.stdout_handler = logging.StreamHandler(sys.stdout)
self.stdout_handler.setFormatter(logging.Formatter("%(level)s:%(message)s"))
@@ -220,6 +232,18 @@ class SocketServer:
self._sub_lock = threading.Lock()
self.command_receivers = {}
self.enable_tls = enable_tls
self.certfile = certfile
self.keyfile = keyfile
if not self.certfile.exists():
raise FileNotFoundError("Certfile not found")
if not self.keyfile.exists():
raise FileNotFoundError("Keyfile not found")
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
# -------------------------
# Socket Server
@@ -253,6 +277,12 @@ class SocketServer:
def _build_tcp_server(self):
manager = self
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
try:
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
except ssl.SSLError as e:
self.logger.fatal("SSL error", exc_info=e)
class TCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
@@ -261,6 +291,14 @@ class SocketServer:
super().__init__(server_address, RequestHandlerClass)
self.manager = manager
if self.manager.enable_tls:
def get_request():
sock, addr = super().get_request()
tls_sock = context.wrap_socket(sock, server_side=True)
return tls_sock, addr
self.get_request = get_request
class Handler(socketserver.BaseRequestHandler):
current_server_record = {
}
@@ -442,7 +480,7 @@ class SocketServer:
return None
class Server:
def __init__(self, name, version, description, command, work_dir, port, host, enable):
def __init__(self, name, version, description, args, work_dir, port, host, enable):
self._stdout_thread = None
# Process
@@ -464,14 +502,14 @@ class Server:
self.logger = logging.getLogger(name)
self.logger.setLevel(logging.INFO)
self.stdout_handler = logging.StreamHandler(sys.stdout)
self.stdout_handler.setFormatter(logging.Formatter("[%(asctime)s:%(level)s]: %(message)s"))
self.stdout_handler.setFormatter(logging.Formatter(f"[%(asctime)s:%(levelname)s:{name}]: %(message)s"))
self.logger.addHandler(self.stdout_handler)
# Values from config
self.name = name
self.version = version
self.description = description
self.command = command
self.args = args
self.work_dir = work_dir
self.port = port
self.host = host
@@ -489,14 +527,12 @@ class Server:
self.logger.warning("[PROC] already running, skip")
return
args = shlex.split(self.command)
if not args:
raise ValueError(f"Server \"{self.name}\" command is empty.")
self.logger.info("[PROC] spawning: %s", self.command)
if len(self.args) == 0:
raise Exception("[SYS] No arguments provided")
self.logger.info("[PROC] spawning: %s", " ".join(self.args))
self.proc = subprocess.Popen(
args,
self.args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
@@ -634,7 +670,7 @@ def load_all_server_from_settings(settings: FileSettings):
name=server_conf.get("name"),
version=server_conf.get("version"),
description=server_conf.get("description"),
command=server_conf.get("command"),
args=server_conf.get("args",[]),
work_dir=server_conf.get("workDir"),
port=server_conf.get("port"),
host=server_conf.get("host"),
@@ -647,7 +683,7 @@ def load_all_server_from_settings(settings: FileSettings):
@main.command()
def runserver():
logger = logging.getLogger(__name__)
formatter = logging.Formatter('%(asctime)s:%(levelname)s: %(message)s')
formatter = logging.Formatter('[%(asctime)s:%(levelname)s:runServer]: %(message)s')
logger.setLevel(logging.INFO)
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
@@ -659,10 +695,11 @@ def runserver():
logger.info("{} servers available".format(len(servers)))
# Socket
logger.info("Starting socket server")
socket_server = SocketServer(settings.get("socketServerHostname", "127.0.0.1"),
settings.get("socketServerPort", 25560))
socket_server.start_socket_server()
settings.get("socketServerPort", 25560),
settings.get("enableTLSSupport", True),
Path(settings.get("socketServerCertfile", "data/server.crt")),
Path(settings.get( "socketServerKeyfile", "data/server.key")))
# Flags
stop_once = False
@@ -693,6 +730,10 @@ def runserver():
)
# Boot server
if len(servers) != 0:
logger.info("Starting socket server")
socket_server.start_socket_server()
logger.info("Starting server")
for server in servers:
if server.enable:
@@ -731,9 +772,66 @@ def runserver():
except KeyboardInterrupt:
logger.info("Stopping server...")
cleanup()
else:
logger.info("No work to do.")
settings.save()
logger.info("Stopped!")
@main.command()
def generate_tls_key():
print("Generating TLS key...")
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"),
x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Dev Org"),
x509.NameAttribute(NameOID.COMMON_NAME, u"localhost"),
])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.now(datetime.UTC)
).not_valid_after(
# Valid for 1 year
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365)
).add_extension(
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]),
critical=False,
).sign(key, hashes.SHA256())
private_key = Path("data/server-private.pem")
public_key = Path("data/server-public.pem")
private_key.parent.mkdir(parents=True, exist_ok=True)
public_key.parent.mkdir(parents=True, exist_ok=True)
with private_key.open("wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
with public_key.open("wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print("Private key saved to {}".format(private_key))
print("Public key saved to {}".format(public_key))
print("Done.")
if __name__ == "__main__":
main()
+29 -3
View File
@@ -9,7 +9,7 @@ MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_man
def download_file(url: str, destination: Path, chunk_size: int = 1024 * 512):
destination.mkdir(parents=True, exist_ok=True)
destination.parent.mkdir(parents=True, exist_ok=True)
with requests.get(url, stream=True, timeout=30) as r:
if r.status_code != 200:
@@ -17,7 +17,7 @@ def download_file(url: str, destination: Path, chunk_size: int = 1024 * 512):
total = int(r.headers.get("content-length", 0))
with destination.open(mode="wb", buffering=chunk_size).write(r.content) as f:
with destination.open(mode="wb", buffering=chunk_size) as f:
if total > 0:
with click.progressbar(length=total, label=f"Downloading {os.path.basename(destination)}") as bar:
for chunk in r.iter_content(chunk_size=chunk_size):
@@ -72,7 +72,7 @@ def get_version_list(release=True):
def get_latest_version_minecraft(release=True):
version_list = get_version_list(release=release)
ver = version_list[0].get("id") if version_list else None
ver = version_list[0] if version_list else None
if ver is None:
raise Exception("Unable to find latest version in version list.\n")
@@ -115,6 +115,31 @@ def download_latest_build_paper_jar(minecraft_version: str, destination_dir: Pat
build = get_latest_build_of_version(minecraft_version)
return download_server_jar(minecraft_version, build, destination_dir, filename=filename)
def version_exist_from_paper(minecraft_version: str) -> bool:
try:
get_specific_version_paper_builds(minecraft_version)
return True
except Exception:
return False
def get_latest_paper_version(release) -> str:
vers = get_version_list(release=release)
index = 0
latest_paper_support_ver = None
while latest_paper_support_ver is None:
if len(vers) < index+1:
raise Exception("No supported Minecraft version available for Paper support.")
if version_exist_from_paper(minecraft_version=vers[index]):
latest_paper_support_ver = vers[index]
break
index+=1
return latest_paper_support_ver
def download_latest_paper_jar(destination_dir: Path, filename: str | None = None, release: bool = True):
"""
@@ -126,4 +151,5 @@ def download_latest_paper_jar(destination_dir: Path, filename: str | None = None
raise Exception("No versions available for Minecraft (Did the server return wrong response ?)")
latest_mc = vers[0]
return download_latest_build_paper_jar(latest_mc, destination_dir, filename=filename)
+7 -1
View File
@@ -270,9 +270,15 @@ class FileSettings:
def _validate_dict_form(self, sample, target):
for k, v in sample.items():
if target.get(k, None) is None:
target[k] = v
target[k] = copy.deepcopy(v)
elif isinstance(v, dict) and isinstance(target.get(k, None), dict):
target[k] = self._validate_dict_form(v, target.get(k, {}))
elif type(target.get(k, None)) is not type(v):
candidate = target[k].get("default") if isinstance(target[k], dict) else None
if type(candidate) is type(v):
target[k] = copy.deepcopy(candidate)
else:
target[k] = copy.deepcopy(v)
return target