Merge branch 'dev' into 'pretzel'

# Conflicts:
#   app/classes/shared/server.py
#   app/classes/web/panel_handler.py
#   main.py
This commit is contained in:
Andrew 2021-08-18 16:34:41 +00:00
commit 0ab8f11a60
24 changed files with 297 additions and 105 deletions

18
README.md Normal file
View File

@ -0,0 +1,18 @@
# Crafty Controller 4.0.0-alpha.2
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
Crafty Controller is a Minecraft Server Control Panel / Launcher. The purpose
of Crafty Controller is to launch a Minecraft Server in the background and present
a web interface for the server administrators to interact with their servers. Crafty
is compatible with Docker, Linux, Windows 7, Windows 8 and Windows 10.
## Documentation
Temporary documentation available on [GitLab](https://gitlab.com/crafty-controller/crafty-commander/wikis/home)
## Meta
Project Homepage - https://craftycontrol.com
Discord Server - https://discord.gg/9VJPhCE
Git Repository - https://gitlab.com/crafty-controller/crafty-web

View File

@ -71,6 +71,29 @@ class ServerJars:
data = self._read_cache() data = self._read_cache()
return data.get('servers') return data.get('servers')
def get_serverjar_data_sorted(self):
data = self.get_serverjar_data()
def str_to_int(x, counter=0):
try:
return ord(x[0]) + str_to_int(x[1:], counter + 1) + len(x)
except IndexError:
return 0
def to_int(x):
try:
return int(x)
except ValueError:
temp = x.split('-')
return to_int(temp[0]) + str_to_int(temp[1]) / 100000
sort_key_fn = lambda x: [to_int(y) for y in x.split('.')]
for key in data.keys():
data[key] = sorted(data[key], key=sort_key_fn)
return data
def _check_api_alive(self): def _check_api_alive(self):
logger.info("Checking serverjars.com API status") logger.info("Checking serverjars.com API status")

View File

@ -316,7 +316,7 @@ class Controller:
server_command = 'java -Xms{}M -Xmx{}M -jar {} nogui'.format(helper.float_to_string(min_mem), server_command = 'java -Xms{}M -Xmx{}M -jar {} nogui'.format(helper.float_to_string(min_mem),
helper.float_to_string(max_mem), helper.float_to_string(max_mem),
full_jar_path) full_jar_path)
print('command: ' + server_command) logger.debug('command: ' + server_command)
server_log_file = "{}/logs/latest.log".format(new_server_dir) server_log_file = "{}/logs/latest.log".format(new_server_dir)
server_stop = "stop" server_stop = "stop"

View File

@ -159,7 +159,7 @@ class Helpers:
version = "{}.{}.{}-{}".format(version_data.get('major', '?'), version = "{}.{}.{}-{}".format(version_data.get('major', '?'),
version_data.get('minor', '?'), version_data.get('minor', '?'),
version_data.get('sub', '?'), version_data.get('sub', '?'),
version_data.get('patch', '?')) version_data.get('meta', '?'))
return str(version) return str(version)
def do_exit(self): def do_exit(self):
@ -195,6 +195,9 @@ class Helpers:
(r'(\[.+?/ERROR\])', r'<span class="mc-log-error">\1</span>'), (r'(\[.+?/ERROR\])', r'<span class="mc-log-error">\1</span>'),
(r'(\w+?\[/\d+?\.\d+?\.\d+?\.\d+?\:\d+?\])', r'<span class="mc-log-keyword">\1</span>'), (r'(\w+?\[/\d+?\.\d+?\.\d+?\.\d+?\:\d+?\])', r'<span class="mc-log-keyword">\1</span>'),
(r'\[(\d\d:\d\d:\d\d)\]', r'<span class="mc-log-time">[\1]</span>'), (r'\[(\d\d:\d\d:\d\d)\]', r'<span class="mc-log-time">[\1]</span>'),
(r'(\[.+? INFO\])', r'<span class="mc-log-info">\1</span>'),
(r'(\[.+? WARN\])', r'<span class="mc-log-warn">\1</span>'),
(r'(\[.+? ERROR\])', r'<span class="mc-log-error">\1</span>')
] ]
# highlight users keywords # highlight users keywords
@ -590,4 +593,10 @@ class Helpers:
return True return True
@staticmethod
def remove_prefix(text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return text
helper = Helpers() helper = Helpers()

View File

@ -11,6 +11,7 @@ import schedule
import logging.config import logging.config
import zipfile import zipfile
from threading import Thread from threading import Thread
import html
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
@ -29,6 +30,48 @@ except ModuleNotFoundError as e:
console.critical("Import Error: Unable to load {} module".format(e.name)) console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1) sys.exit(1)
class ServerOutBuf:
lines = {}
def __init__(self, p, server_id):
self.p = p
self.server_id = str(server_id)
# Buffers text for virtual_terminal_lines config number of lines
self.max_lines = helper.get_setting('virtual_terminal_lines')
self.line_buffer = ''
ServerOutBuf.lines[self.server_id] = []
def check(self):
while self.p.isalive():
char = self.p.read(1)
if char == os.linesep:
ServerOutBuf.lines[self.server_id].append(self.line_buffer)
self.new_line_handler(self.line_buffer)
self.line_buffer = ''
# Limit list length to self.max_lines:
if len(ServerOutBuf.lines[self.server_id]) > self.max_lines:
ServerOutBuf.lines[self.server_id].pop(0)
else:
self.line_buffer += char
def new_line_handler(self, new_line):
new_line = re.sub('(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)|(> )', '', new_line)
new_line = re.sub('[A-z]{2}\b\b', '', new_line)
highlighted = helper.log_colors(html.escape(new_line))
logger.debug('Broadcasting new virtual terminal line')
# TODO: Do not send data to clients who do not have permission to view this server's console
websocket_helper.broadcast_page_params(
'/panel/server_detail',
{
'id': self.server_id
},
'vterm_new_line',
{
'line': highlighted + '<br />'
}
)
class Server: class Server:
@ -88,7 +131,7 @@ class Server:
def run_threaded_server(self): def run_threaded_server(self):
# start the server # start the server
self.server_thread = threading.Thread(target=self.start_server, daemon=True) self.server_thread = threading.Thread(target=self.start_server, daemon=True, name='{}_server_thread'.format(self.server_id))
self.server_thread.start() self.server_thread.start()
def setup_server_run_command(self): def setup_server_run_command(self):
@ -137,6 +180,7 @@ class Server:
logger.info("Linux Detected") logger.info("Linux Detected")
logger.info("Starting server in {p} with command: {c}".format(p=self.server_path, c=self.server_command)) logger.info("Starting server in {p} with command: {c}".format(p=self.server_path, c=self.server_command))
<<<<<<< app/classes/shared/server.py
try: try:
self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None) self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None)
except Exception as ex: except Exception as ex:
@ -148,6 +192,15 @@ class Server:
return False return False
websocket_helper.broadcast('send_start_reload', { websocket_helper.broadcast('send_start_reload', {
}) })
=======
self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding='utf-8')
out_buf = ServerOutBuf(self.process, self.server_id)
logger.debug('Starting virtual terminal listener for server {}'.format(self.name))
threading.Thread(target=out_buf.check, daemon=True, name='{}_virtual_terminal'.format(self.server_id)).start()
>>>>>>> app/classes/shared/server.py
self.is_crashed = False self.is_crashed = False
self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')) self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'))

View File

@ -210,7 +210,7 @@ class TasksManager:
host_stats = db_helper.get_latest_hosts_stats() host_stats = db_helper.get_latest_hosts_stats()
if len(websocket_helper.clients) > 0: if len(websocket_helper.clients) > 0:
# There are clients # There are clients
websocket_helper.broadcast('update_host_stats', { websocket_helper.broadcast_page('/panel/dashboard', 'update_host_stats', {
'cpu_usage': host_stats.get('cpu_usage'), 'cpu_usage': host_stats.get('cpu_usage'),
'cpu_cores': host_stats.get('cpu_cores'), 'cpu_cores': host_stats.get('cpu_cores'),
'cpu_cur_freq': host_stats.get('cpu_cur_freq'), 'cpu_cur_freq': host_stats.get('cpu_cur_freq'),
@ -218,13 +218,9 @@ class TasksManager:
'mem_percent': host_stats.get('mem_percent'), 'mem_percent': host_stats.get('mem_percent'),
'mem_usage': host_stats.get('mem_usage') 'mem_usage': host_stats.get('mem_usage')
}) })
time.sleep(4) time.sleep(4)
else:
# Stats are same
time.sleep(8)
def log_watcher(self): def log_watcher(self):
console.debug('in log_watcher')
helper.check_for_old_logs(db_helper) helper.check_for_old_logs(db_helper)
schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper)).tag('log-mgmt') schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper)).tag('log-mgmt')

View File

@ -13,19 +13,19 @@ class Translation():
self.translations_path = os.path.join(helper.root_dir, 'app', 'translations') self.translations_path = os.path.join(helper.root_dir, 'app', 'translations')
self.cached_translation = None self.cached_translation = None
self.cached_translation_lang = None self.cached_translation_lang = None
self.lang_file_exists = []
def translate(self, page, word): def translate(self, page, word):
translated_word = None translated_word = None
lang = helper.get_setting('language') lang = helper.get_setting('language')
fallback_lang = 'en_EN' fallback_lang = 'en_EN'
lang_file_exists = helper.check_file_exists( if lang not in self.lang_file_exists and \
os.path.join( helper.check_file_exists(os.path.join(self.translations_path, lang + '.json')):
self.translations_path, lang + '.json' self.lang_file_exists.append(lang)
)
)
translated_word = self.translate_inner(page, word, lang) \ translated_word = self.translate_inner(page, word, lang) \
if lang_file_exists else self.translate_inner(page, word, fallback_lang) if lang in self.lang_file_exists else self.translate_inner(page, word, fallback_lang)
if translated_word: if translated_word:
if isinstance(translated_word, dict): return json.dumps(translated_word) if isinstance(translated_word, dict): return json.dumps(translated_word)

View File

@ -5,12 +5,15 @@ import tornado.escape
import bleach import bleach
import os import os
import shutil import shutil
import html
import re
from app.classes.shared.console import console from app.classes.shared.console import console
from app.classes.shared.models import Users, installer from app.classes.shared.models import Users, installer
from app.classes.web.base_handler import BaseHandler from app.classes.web.base_handler import BaseHandler
from app.classes.shared.models import db_helper from app.classes.shared.models import db_helper
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.shared.server import ServerOutBuf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -47,7 +50,7 @@ class AjaxHandler(BaseHandler):
if server_id is None: if server_id is None:
logger.warning("Server ID not found in server_log ajax call") logger.warning("Server ID not found in server_log ajax call")
self.redirect("/panel/error?error=Server ID Not Found") self.redirect("/panel/error?error=Server ID Not Found")
return False return
server_id = bleach.clean(server_id) server_id = bleach.clean(server_id)
@ -55,20 +58,23 @@ class AjaxHandler(BaseHandler):
if not server_data: if not server_data:
logger.warning("Server Data not found in server_log ajax call") logger.warning("Server Data not found in server_log ajax call")
self.redirect("/panel/error?error=Server ID Not Found") self.redirect("/panel/error?error=Server ID Not Found")
return
if not server_data['log_path']: if not server_data['log_path']:
logger.warning("Log path not found in server_log ajax call ({})".format(server_id)) logger.warning("Log path not found in server_log ajax call ({})".format(server_id))
if full_log: if full_log:
log_lines = helper.get_setting('max_log_lines') log_lines = helper.get_setting('max_log_lines')
data = helper.tail_file(server_data['log_path'], log_lines)
else: else:
log_lines = helper.get_setting('virtual_terminal_lines') data = ServerOutBuf.lines.get(server_id, [])
data = helper.tail_file(server_data['log_path'], log_lines)
for d in data: for d in data:
try: try:
line = helper.log_colors(d) d = re.sub('(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)|(> )', '', d)
d = re.sub('[A-z]{2}\b\b', '', d)
line = helper.log_colors(html.escape(d))
self.write('{}<br />'.format(line)) self.write('{}<br />'.format(line))
# self.write(d.encode("utf-8")) # self.write(d.encode("utf-8"))
@ -85,14 +91,14 @@ class AjaxHandler(BaseHandler):
file_path = self.get_argument('file_path', None) file_path = self.get_argument('file_path', None)
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'get_file'): return False if not self.check_server_id(server_id, 'get_file'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path)\ if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path)\
or not helper.check_file_exists(os.path.abspath(file_path)): or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in get_file ajax call ({})".format(file_path)) logger.warning("Invalid path in get_file ajax call ({})".format(file_path))
console.warning("Invalid path in get_file ajax call ({})".format(file_path)) console.warning("Invalid path in get_file ajax call ({})".format(file_path))
return False return
error = None error = None
@ -113,7 +119,7 @@ class AjaxHandler(BaseHandler):
elif page == "get_tree": elif page == "get_tree":
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'get_tree'): return False if not self.check_server_id(server_id, 'get_tree'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
self.write(db_helper.get_server_data_by_id(server_id)['path'] + '\n' + self.write(db_helper.get_server_data_by_id(server_id)['path'] + '\n' +
@ -149,16 +155,15 @@ class AjaxHandler(BaseHandler):
file_name = self.get_body_argument('file_name', default=None, strip=True) file_name = self.get_body_argument('file_name', default=None, strip=True)
file_path = os.path.join(file_parent, file_name) file_path = os.path.join(file_parent, file_name)
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
print(server_id)
if not self.check_server_id(server_id, 'create_file'): return False if not self.check_server_id(server_id, 'create_file'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path) \ if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path) \
or helper.check_file_exists(os.path.abspath(file_path)): or helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in create_file ajax call ({})".format(file_path)) logger.warning("Invalid path in create_file ajax call ({})".format(file_path))
console.warning("Invalid path in create_file ajax call ({})".format(file_path)) console.warning("Invalid path in create_file ajax call ({})".format(file_path))
return False return
# Create the file by opening it # Create the file by opening it
with open(file_path, 'w') as file_object: with open(file_path, 'w') as file_object:
@ -169,16 +174,15 @@ class AjaxHandler(BaseHandler):
dir_name = self.get_body_argument('dir_name', default=None, strip=True) dir_name = self.get_body_argument('dir_name', default=None, strip=True)
dir_path = os.path.join(dir_parent, dir_name) dir_path = os.path.join(dir_parent, dir_name)
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
print(server_id)
if not self.check_server_id(server_id, 'create_dir'): return False if not self.check_server_id(server_id, 'create_dir'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], dir_path) \ if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], dir_path) \
or helper.check_path_exists(os.path.abspath(dir_path)): or helper.check_path_exists(os.path.abspath(dir_path)):
logger.warning("Invalid path in create_dir ajax call ({})".format(dir_path)) logger.warning("Invalid path in create_dir ajax call ({})".format(dir_path))
console.warning("Invalid path in create_dir ajax call ({})".format(dir_path)) console.warning("Invalid path in create_dir ajax call ({})".format(dir_path))
return False return
# Create the directory # Create the directory
os.mkdir(dir_path) os.mkdir(dir_path)
@ -191,7 +195,7 @@ class AjaxHandler(BaseHandler):
console.warning("delete {} for server {}".format(file_path, server_id)) console.warning("delete {} for server {}".format(file_path, server_id))
if not self.check_server_id(server_id, 'del_file'): return False if not self.check_server_id(server_id, 'del_file'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
server_info = db_helper.get_server_data_by_id(server_id) server_info = db_helper.get_server_data_by_id(server_id)
@ -200,7 +204,7 @@ class AjaxHandler(BaseHandler):
or not helper.check_file_exists(os.path.abspath(file_path)): or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in del_file ajax call ({})".format(file_path)) logger.warning("Invalid path in del_file ajax call ({})".format(file_path))
console.warning("Invalid path in del_file ajax call ({})".format(file_path)) console.warning("Invalid path in del_file ajax call ({})".format(file_path))
return False return
# Delete the file # Delete the file
os.remove(file_path) os.remove(file_path)
@ -208,11 +212,10 @@ class AjaxHandler(BaseHandler):
elif page == "del_dir": elif page == "del_dir":
dir_path = self.get_body_argument('dir_path', default=None, strip=True) dir_path = self.get_body_argument('dir_path', default=None, strip=True)
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
print(server_id)
console.warning("delete {} for server {}".format(dir_path, server_id)) console.warning("delete {} for server {}".format(dir_path, server_id))
if not self.check_server_id(server_id, 'del_dir'): return False if not self.check_server_id(server_id, 'del_dir'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
server_info = db_helper.get_server_data_by_id(server_id) server_info = db_helper.get_server_data_by_id(server_id)
@ -220,7 +223,7 @@ class AjaxHandler(BaseHandler):
or not helper.check_path_exists(os.path.abspath(dir_path)): or not helper.check_path_exists(os.path.abspath(dir_path)):
logger.warning("Invalid path in del_file ajax call ({})".format(dir_path)) logger.warning("Invalid path in del_file ajax call ({})".format(dir_path))
console.warning("Invalid path in del_file ajax call ({})".format(dir_path)) console.warning("Invalid path in del_file ajax call ({})".format(dir_path))
return False return
# Delete the directory # Delete the directory
# os.rmdir(dir_path) # Would only remove empty directories # os.rmdir(dir_path) # Would only remove empty directories
@ -232,18 +235,15 @@ class AjaxHandler(BaseHandler):
file_contents = self.get_body_argument('file_contents', default=None, strip=True) file_contents = self.get_body_argument('file_contents', default=None, strip=True)
file_path = self.get_body_argument('file_path', default=None, strip=True) file_path = self.get_body_argument('file_path', default=None, strip=True)
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
print(file_contents)
print(file_path)
print(server_id)
if not self.check_server_id(server_id, 'save_file'): return False if not self.check_server_id(server_id, 'save_file'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path)\ if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path)\
or not helper.check_file_exists(os.path.abspath(file_path)): or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in save_file ajax call ({})".format(file_path)) logger.warning("Invalid path in save_file ajax call ({})".format(file_path))
console.warning("Invalid path in save_file ajax call ({})".format(file_path)) console.warning("Invalid path in save_file ajax call ({})".format(file_path))
return False return
# Open the file in write mode and store the content in file_object # Open the file in write mode and store the content in file_object
with open(file_path, 'w') as file_object: with open(file_path, 'w') as file_object:
@ -253,21 +253,20 @@ class AjaxHandler(BaseHandler):
item_path = self.get_body_argument('item_path', default=None, strip=True) item_path = self.get_body_argument('item_path', default=None, strip=True)
new_item_name = self.get_body_argument('new_item_name', default=None, strip=True) new_item_name = self.get_body_argument('new_item_name', default=None, strip=True)
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
print(server_id)
if not self.check_server_id(server_id, 'rename_item'): return False if not self.check_server_id(server_id, 'rename_item'): return
else: server_id = bleach.clean(server_id) else: server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None: if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_item ajax call") logger.warning("Invalid path(s) in rename_item ajax call")
console.warning("Invalid path(s) in rename_item ajax call") console.warning("Invalid path(s) in rename_item ajax call")
return False return
if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], item_path) \ if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], item_path) \
or not helper.check_path_exists(os.path.abspath(item_path)): or not helper.check_path_exists(os.path.abspath(item_path)):
logger.warning("Invalid old name path in rename_item ajax call ({})".format(server_id)) logger.warning("Invalid old name path in rename_item ajax call ({})".format(server_id))
console.warning("Invalid old name path in rename_item ajax call ({})".format(server_id)) console.warning("Invalid old name path in rename_item ajax call ({})".format(server_id))
return False return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name) new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
@ -275,7 +274,7 @@ class AjaxHandler(BaseHandler):
or helper.check_path_exists(os.path.abspath(new_item_path)): or helper.check_path_exists(os.path.abspath(new_item_path)):
logger.warning("Invalid new name path in rename_item ajax call ({})".format(server_id)) logger.warning("Invalid new name path in rename_item ajax call ({})".format(server_id))
console.warning("Invalid new name path in rename_item ajax call ({})".format(server_id)) console.warning("Invalid new name path in rename_item ajax call ({})".format(server_id))
return False return
# RENAME # RENAME
os.rename(item_path, new_item_path) os.rename(item_path, new_item_path)
@ -283,7 +282,7 @@ class AjaxHandler(BaseHandler):
if server_id is None: if server_id is None:
logger.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id)) logger.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id))
console.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id)) console.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id))
return False return
else: else:
server_id = bleach.clean(server_id) server_id = bleach.clean(server_id)
@ -291,5 +290,5 @@ class AjaxHandler(BaseHandler):
if not db_helper.server_id_exists(server_id): if not db_helper.server_id_exists(server_id):
logger.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id)) logger.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id))
console.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id)) console.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id))
return False return
return True return True

View File

@ -41,11 +41,11 @@ class ApiHandler(BaseHandler):
else: else:
logging.debug("Auth unsuccessful") logging.debug("Auth unsuccessful")
self.access_denied("unknown", "the user provided an invalid token") self.access_denied("unknown", "the user provided an invalid token")
return False return
except Exception as e: except Exception as e:
log.warning("An error occured while authenticating an API user: %s", e) log.warning("An error occured while authenticating an API user: %s", e)
self.access_denied("unknown"), "an error occured while authenticating the user" self.access_denied("unknown"), "an error occured while authenticating the user"
return False return
class ServersStats(ApiHandler): class ServersStats(ApiHandler):

View File

@ -201,7 +201,7 @@ class PanelHandler(BaseHandler):
#if not db_helper.server_id_authorized(server_id, exec_user_id): #if not db_helper.server_id_authorized(server_id, exec_user_id):
if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id): if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id):
self.redirect("/panel/error?error=Invalid Server ID") self.redirect("/panel/error?error=Invalid Server ID")
return False return
server_info = db_helper.get_server_data_by_id(server_id) server_info = db_helper.get_server_data_by_id(server_id)
backup_file = os.path.abspath(os.path.join(server_info["backup_path"], file)) backup_file = os.path.abspath(os.path.join(server_info["backup_path"], file))
@ -250,7 +250,7 @@ class PanelHandler(BaseHandler):
#if not db_helper.server_id_authorized(server_id, exec_user_id): #if not db_helper.server_id_authorized(server_id, exec_user_id):
if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id): if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id):
self.redirect("/panel/error?error=Invalid Server ID") self.redirect("/panel/error?error=Invalid Server ID")
return False return
server = self.controller.get_server_obj(server_id).backup_server() server = self.controller.get_server_obj(server_id).backup_server()
self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id))

View File

@ -37,9 +37,6 @@ class PublicHandler(BaseHandler):
def get(self, page=None): def get(self, page=None):
self.clear_cookie("user")
self.clear_cookie("user_data")
error = bleach.clean(self.get_argument('error', "Invalid Login!")) error = bleach.clean(self.get_argument('error', "Invalid Login!"))
page_data = { page_data = {
@ -59,9 +56,16 @@ class PublicHandler(BaseHandler):
elif page == "error": elif page == "error":
template = "public/error.html" template = "public/error.html"
elif page == "logout":
self.clear_cookie("user")
self.clear_cookie("user_data")
self.redirect('/public/login')
return
# if we have no page, let's go to login # if we have no page, let's go to login
else: else:
self.redirect('/public/login') self.redirect('/public/login')
return
self.render( self.render(
template, template,
@ -82,14 +86,18 @@ class PublicHandler(BaseHandler):
# if we don't have a user # if we don't have a user
if not user_data: if not user_data:
next_page = "/public/error?error=Login Failed" next_page = "/public/error?error=Login Failed"
self.clear_cookie("user")
self.clear_cookie("user_data")
self.redirect(next_page) self.redirect(next_page)
return False return
# if they are disabled # if they are disabled
if not user_data.enabled: if not user_data.enabled:
next_page = "/public/error?error=Login Failed" next_page = "/public/error?error=Login Failed"
self.clear_cookie("user")
self.clear_cookie("user_data")
self.redirect(next_page) self.redirect(next_page)
return False return
login_result = helper.verify_pass(entered_password, user_data.password) login_result = helper.verify_pass(entered_password, user_data.password)
@ -118,6 +126,8 @@ class PublicHandler(BaseHandler):
next_page = "/panel/dashboard" next_page = "/panel/dashboard"
self.redirect(next_page) self.redirect(next_page)
else: else:
self.clear_cookie("user")
self.clear_cookie("user_data")
# log this failed login attempt # log this failed login attempt
db_helper.add_to_audit_log(user_data.user_id, "Tried to log in", 0, self.get_remote_ip()) db_helper.add_to_audit_log(user_data.user_id, "Tried to log in", 0, self.get_remote_ip())
self.redirect('/public/error?error=Login Failed') self.redirect('/public/error?error=Login Failed')

View File

@ -61,7 +61,7 @@ class ServerHandler(BaseHandler):
if page == "step1": if page == "step1":
page_data['server_types'] = server_jar_obj.get_serverjar_data() page_data['server_types'] = server_jar_obj.get_serverjar_data_sorted()
template = "server/wizard.html" template = "server/wizard.html"
self.render( self.render(
@ -94,7 +94,7 @@ class ServerHandler(BaseHandler):
for server in db_helper.get_all_defined_servers(): for server in db_helper.get_all_defined_servers():
if server['server_name'] == name: if server['server_name'] == name:
return True return True
return False return
server_data = db_helper.get_server_data_by_id(server_id) server_data = db_helper.get_server_data_by_id(server_id)
server_uuid = server_data.get('server_uuid') server_uuid = server_data.get('server_uuid')
@ -105,8 +105,6 @@ class ServerHandler(BaseHandler):
name_counter += 1 name_counter += 1
new_server_name = server_data.get('server_name') + " (Copy {})".format(name_counter) new_server_name = server_data.get('server_name') + " (Copy {})".format(name_counter)
console.debug('new_server_name: "{}"'.format(new_server_name))
new_server_uuid = helper.create_uuid() new_server_uuid = helper.create_uuid()
while os.path.exists(os.path.join(helper.servers_dir, new_server_uuid)): while os.path.exists(os.path.join(helper.servers_dir, new_server_uuid)):
new_server_uuid = helper.create_uuid() new_server_uuid = helper.create_uuid()
@ -143,7 +141,6 @@ class ServerHandler(BaseHandler):
}).execute() }).execute()
self.controller.init_all_servers() self.controller.init_all_servers()
console.debug('initted all servers')
return return
@ -163,14 +160,14 @@ class ServerHandler(BaseHandler):
if not server_name: if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!") self.redirect("/panel/error?error=Server name cannot be empty!")
return False return
if import_type == 'import_jar': if import_type == 'import_jar':
good_path = self.controller.verify_jar_server(import_server_path, import_server_jar) good_path = self.controller.verify_jar_server(import_server_path, import_server_jar)
if not good_path: if not good_path:
self.redirect("/panel/error?error=Server path or Server Jar not found!") self.redirect("/panel/error?error=Server path or Server Jar not found!")
return False return
new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port)
db_helper.add_to_audit_log(exec_user_data['user_id'], db_helper.add_to_audit_log(exec_user_data['user_id'],
@ -182,12 +179,12 @@ class ServerHandler(BaseHandler):
good_path = self.controller.verify_zip_server(import_server_path) good_path = self.controller.verify_zip_server(import_server_path)
if not good_path: if not good_path:
self.redirect("/panel/error?error=Zip file not found!") self.redirect("/panel/error?error=Zip file not found!")
return False return
new_server_id = self.controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) new_server_id = self.controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port)
if new_server_id == "false": if new_server_id == "false":
self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path)) self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path))
return False return
db_helper.add_to_audit_log(exec_user_data['user_id'], db_helper.add_to_audit_log(exec_user_data['user_id'],
"imported a zip server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative" "imported a zip server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative"
new_server_id, new_server_id,
@ -195,7 +192,7 @@ class ServerHandler(BaseHandler):
else: else:
if len(server_parts) != 2: if len(server_parts) != 2:
self.redirect("/panel/error?error=Invalid server data") self.redirect("/panel/error?error=Invalid server data")
return False return
server_type, server_version = server_parts server_type, server_version = server_parts
# todo: add server type check here and call the correct server add functions if not a jar # todo: add server type check here and call the correct server add functions if not a jar
new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port) new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port)

View File

@ -159,7 +159,7 @@ class Webserver:
console.info("Server Init Complete: Listening For Connections:") console.info("Server Init Complete: Listening For Connections:")
self.ioloop = tornado.ioloop.IOLoop.instance() self.ioloop = tornado.ioloop.IOLoop.current()
self.ioloop.start() self.ioloop.start()
def stop_web_server(self): def stop_web_server(self):

View File

@ -1,13 +1,21 @@
import json import json
import logging import logging
import asyncio
import tornado.websocket from urllib.parse import parse_qsl
from app.classes.shared.console import console
from app.classes.shared.models import Users, db_helper from app.classes.shared.models import Users, db_helper
from app.classes.shared.helpers import helper
from app.classes.web.websocket_helper import websocket_helper from app.classes.web.websocket_helper import websocket_helper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try:
import tornado.websocket
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
console.critical("Import Error: Unable to load {} module".format(e, e.name))
sys.exit(1)
class SocketHandler(tornado.websocket.WebSocketHandler): class SocketHandler(tornado.websocket.WebSocketHandler):
@ -15,6 +23,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.io_loop = tornado.ioloop.IOLoop.current()
def get_remote_ip(self): def get_remote_ip(self):
remote_ip = self.request.headers.get("X-Real-IP") or \ remote_ip = self.request.headers.get("X-Real-IP") or \
@ -35,6 +44,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
def open(self): def open(self):
logger.debug('Checking WebSocket authentication')
if self.check_auth(): if self.check_auth():
self.handle() self.handle()
else: else:
@ -42,10 +52,15 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
self.close() self.close()
db_helper.add_to_audit_log_raw('unknown', 0, 0, 'Someone tried to connect via WebSocket without proper authentication', self.get_remote_ip()) db_helper.add_to_audit_log_raw('unknown', 0, 0, 'Someone tried to connect via WebSocket without proper authentication', self.get_remote_ip())
websocket_helper.broadcast('notification', 'Someone tried to connect via WebSocket without proper authentication') websocket_helper.broadcast('notification', 'Someone tried to connect via WebSocket without proper authentication')
logger.warning('Someone tried to connect via WebSocket without proper authentication')
def handle(self): def handle(self):
self.page = self.get_query_argument('page')
websocket_helper.addClient(self) self.page_query_params = dict(parse_qsl(helper.remove_prefix(
self.get_query_argument('page_query_params'),
'?'
)))
websocket_helper.add_client(self)
logger.debug('Opened WebSocket connection') logger.debug('Opened WebSocket connection')
# websocket_helper.broadcast('notification', 'New client connected') # websocket_helper.broadcast('notification', 'New client connected')
@ -56,7 +71,13 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data'])) logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data']))
def on_close(self): def on_close(self):
websocket_helper.removeClient(self) websocket_helper.remove_client(self)
logger.debug('Closed WebSocket connection') logger.debug('Closed WebSocket connection')
# websocket_helper.broadcast('notification', 'Client disconnected') # websocket_helper.broadcast('notification', 'Client disconnected')
async def write_message_int(self, message):
self.write_message(message)
def write_message_helper(self, message):
asyncio.run_coroutine_threadsafe(self.write_message_int(message), self.io_loop.asyncio_loop)

View File

@ -1,31 +1,75 @@
import json import json
import logging import logging
import sys, threading, asyncio
from app.classes.shared.console import console from app.classes.shared.console import console
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WebSocketHelper:
clients = set()
def addClient(self, client): try:
import tornado.ioloop
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
console.critical("Import Error: Unable to load {} module".format(e, e.name))
sys.exit(1)
class WebSocketHelper:
def __init__(self):
self.clients = set()
def add_client(self, client):
self.clients.add(client) self.clients.add(client)
def removeClient(self, client): def remove_client(self, client):
self.clients.add(client) self.clients.remove(client)
def send_message(self, client, event_type, data): def send_message(self, client, event_type: str, data):
if client.check_auth(): if client.check_auth():
message = str(json.dumps({'event': event_type, 'data': data})) message = str(json.dumps({'event': event_type, 'data': data}))
client.write_message(message) client.write_message_helper(message)
def broadcast(self, event_type, data): def broadcast(self, event_type: str, data):
logger.debug('Sending: ' + str(json.dumps({'event': event_type, 'data': data}))) logger.debug('Sending to {} clients: {}'.format(len(self.clients), json.dumps({'event': event_type, 'data': data})))
for client in self.clients: for client in self.clients:
try: try:
self.send_message(client, event_type, data) self.send_message(client, event_type, data)
except: except Exception as e:
pass logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip()))
def broadcast_page(self, page: str, event_type: str, data):
def filter_fn(client):
return client.page == page
clients = list(filter(filter_fn, self.clients))
logger.debug('Sending to {} out of {} clients: {}'.format(len(clients), len(self.clients), json.dumps({'event': event_type, 'data': data})))
for client in clients:
try:
self.send_message(client, event_type, data)
except Exception as e:
logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip()))
def broadcast_page_params(self, page: str, params: dict, event_type: str, data):
def filter_fn(client):
if client.page != page:
return False
for key, param in params.items():
if param != client.page_query_params.get(key, None):
return False
return True
clients = list(filter(filter_fn, self.clients))
logger.debug('Sending to {} out of {} clients: {}'.format(len(clients), len(self.clients), json.dumps({'event': event_type, 'data': data})))
for client in clients:
try:
self.send_message(client, event_type, data)
except Exception as e:
logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip()))
def disconnect_all(self): def disconnect_all(self):
console.info('Disconnecting WebSocket clients') console.info('Disconnecting WebSocket clients')

View File

@ -10,7 +10,7 @@
"stats_update_frequency": 30, "stats_update_frequency": 30,
"delete_default_json": false, "delete_default_json": false,
"show_contribute_link": true, "show_contribute_link": true,
"virtual_terminal_lines": 10, "virtual_terminal_lines": 70,
"max_log_lines": 700, "max_log_lines": 700,
"keywords": ["help", "chunk"] "keywords": ["help", "chunk"]
} }

View File

@ -8,7 +8,7 @@
"tornado_access": { "tornado_access": {
"format": "%(asctime)s - [Tornado] - [Access] - %(levelname)s - %(message)s" "format": "%(asctime)s - [Tornado] - [Access] - %(levelname)s - %(message)s"
}, },
"schedule": { "schedule": {
"format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s" "format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s"
} }
}, },

View File

@ -173,8 +173,9 @@
let listenEvents = []; let listenEvents = [];
try { try {
pageQueryParams = 'page_query_params=' + encodeURIComponent(location.search)
var wsInternal = new WebSocket('wss://' + location.host + '/ws'); page = 'page=' + encodeURIComponent(location.pathname)
var wsInternal = new WebSocket('wss://' + location.host + '/ws?' + page + '&' + pageQueryParams);
wsInternal.onopen = function() { wsInternal.onopen = function() {
console.log('opened WebSocket connection:', wsInternal) console.log('opened WebSocket connection:', wsInternal)
}; };

View File

@ -29,7 +29,7 @@
{% end %} {% end %}
</div> </div>
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i> Activity</a> <a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i> Activity</a>
<a class="dropdown-item" href="/public/login"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>Sign Out</a> <a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>Sign Out</a>
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -166,8 +166,9 @@
margin-left: 10px; margin-left: 10px;
} }
/* Style the caret/arrow */ /* Style the items */
.tree-caret { .tree-item,
.files-tree-title {
cursor: pointer; cursor: pointer;
user-select: none; /* Prevent text selection */ user-select: none; /* Prevent text selection */
} }

View File

@ -65,7 +65,7 @@
<li class="nav-item term-nav-item"> <li class="nav-item term-nav-item">
<label class="p-0 m-0"> <label class="p-0 m-0">
<input type="checkbox" name="stop_scroll" id="stop_scroll" /> <input type="checkbox" name="stop_scroll" id="stop_scroll" />
{{ translate('serverTerm', 'stopRefresh') }} {{ translate('serverTerm', 'stopScroll') }}
</label> </label>
</li> </li>
</ul> </ul>
@ -169,7 +169,6 @@
let server_id = '{{ data['server_stats']['server_id']['server_id'] }}'; let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
function get_server_log(){ function get_server_log(){
if( !$("#stop_scroll").is(':checked')){
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/ajax/server_log?id={{ data['server_stats']['server_id']['server_id'] }}', url: '/ajax/server_log?id={{ data['server_stats']['server_id']['server_id'] }}',
@ -177,10 +176,16 @@
success: function (data) { success: function (data) {
console.log('Got Log From Server') console.log('Got Log From Server')
$('#virt_console').html(data); $('#virt_console').html(data);
scroll(); scrollConsole();
}, },
}); });
} }
function new_line_handler(data) {
$('#virt_console').append(data.line)
if (!$("#stop_scroll").is(':checked')) {
scrollConsole()
}
} }
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security //used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
@ -193,9 +198,7 @@
console.log( "ready!" ); console.log( "ready!" );
get_server_log() get_server_log()
setInterval(function(){ webSocket.on('vterm_new_line', new_line_handler)
get_server_log() // this will run after every 5 seconds
}, 1500);
}); });
$('#server_command').on('keydown', function (e) { $('#server_command').on('keydown', function (e) {
@ -218,7 +221,7 @@
}); });
function scroll(){ function scrollConsole(){
var logview = $('#virt_console'); var logview = $('#virt_console');
if(logview.length) if(logview.length)
logview.scrollTop(logview[0].scrollHeight - logview.height()); logview.scrollTop(logview[0].scrollHeight - logview.height());

View File

@ -8,7 +8,7 @@
"error": { "error": {
"hereIsTheError": "Here is the error", "hereIsTheError": "Here is the error",
"contact": "Contact Crafty Control Support via Discord", "contact": "Contact Crafty Control Support via Discord",
"terribleFailure": "What a Terrble Failure!", "terribleFailure": "What a Terrible Failure!",
"embarassing": "Oh my, well, this is embarrassing.", "embarassing": "Oh my, well, this is embarrassing.",
"error": "Error!" "error": "Error!"
}, },
@ -120,7 +120,7 @@
"playerControls": "Player Management" "playerControls": "Player Management"
}, },
"serverTerm": { "serverTerm": {
"stopRefresh": "Stop Refresh", "stopScroll": "Stop Auto Scrollling",
"commandInput": "Enter your command", "commandInput": "Enter your command",
"sendCommand": "Send command", "sendCommand": "Send command",
"start": "Start", "start": "Start",

View File

@ -120,7 +120,7 @@
"playerControls": "Pelaajahallinta" "playerControls": "Pelaajahallinta"
}, },
"serverTerm": { "serverTerm": {
"stopRefresh": "Lopeta päivitys", "stopScroll": "Lopeta automaattinen vieritys",
"commandInput": "Kirjoita komento", "commandInput": "Kirjoita komento",
"sendCommand": "Lähetä komento", "sendCommand": "Lähetä komento",
"start": "Käynnistä", "start": "Käynnistä",

23
main.py
View File

@ -4,6 +4,7 @@ import json
import time import time
import argparse import argparse
import logging.config import logging.config
import signal
""" Our custom classes / pip packages """ """ Our custom classes / pip packages """
from app.classes.shared.console import console from app.classes.shared.console import console
@ -130,9 +131,24 @@ if __name__ == '__main__':
# this should always be last # this should always be last
tasks_manager.start_main_kill_switch_watcher() tasks_manager.start_main_kill_switch_watcher()
Crafty = MainPrompt(tasks_manager, migration_manager) Crafty = MainPrompt(tasks_manager)
def sigterm_handler(signum, current_stack_frame):
print() # for newline
logger.info("Recieved SIGTERM, stopping Crafty")
console.info("Recieved SIGTERM, stopping Crafty")
Crafty.universal_exit()
signal.signal(signal.SIGTERM, sigterm_handler)
if not args.daemon: if not args.daemon:
Crafty.cmdloop() try:
Crafty.cmdloop()
except KeyboardInterrupt:
print() # for newline
logger.info("Recieved SIGINT, stopping Crafty")
console.info("Recieved SIGINT, stopping Crafty")
Crafty.universal_exit()
else: else:
print("Crafty started in daemon mode, no shell will be printed") print("Crafty started in daemon mode, no shell will be printed")
while True: while True:
@ -142,6 +158,7 @@ if __name__ == '__main__':
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Recieved SIGINT, stopping Crafty") logger.info("Recieved SIGINT, stopping Crafty")
console.info("Recieved SIGINT, stopping Crafty")
break break
Crafty.universal_exit() Crafty.universal_exit()