commit 553754cd776253fc6aaf21a470f51ac9ad6ac6f3 Author: BitHeaven Date: Mon Mar 25 09:32:11 2024 +0500 Wipe branch diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d381cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..edfa1fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +BIT License + +Copyright (c) 2023 Bit.Corp + +Permission is deny, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..132bdf3 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Bit.ASICmon-a + +## Monitoring app +yes diff --git a/TODO b/TODO new file mode 100644 index 0000000..028a4d9 --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +MAKE BACKUPS FOR LICENSE SERVER!!! +Enable LED when WARN or CRIT +Switch debug remotely diff --git a/asics.py b/asics.py new file mode 100644 index 0000000..05b2e9d --- /dev/null +++ b/asics.py @@ -0,0 +1,27 @@ +from enum import Enum, auto + +class ASICs(Enum): + # Avalon 10-13 + Avalon1066 = auto() + Avalon1126 = auto() + + # Antminer S + AntminerS19 = auto() + AntminerS19J = auto() + AntminerS19Pro = auto() + AntminerS19JPro = auto() + + # Antminer L + AntminerL3plus = auto() + AntminerL7 = auto() + + # Antminer T + AntminerT17 = auto() + AntminerT17plus = auto() + + # WhatsMiner + WhatsMinerM20s = auto() + WhatsMinerM21s = auto() + WhatsMinerM30 = auto() + WhatsMinerM31 = auto() + WhatsMinerM50 = auto() diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7b2282a --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +pyarmor-7 pack --clean -e "--onefile " main.py +#pyarmor-7 pack --clean -e "--onefile " main.py asics.py checker.py colors.py config.py license.py macros.py telegrambot.py updater.py webserver.py diff --git a/checker.py b/checker.py new file mode 100644 index 0000000..bece0bd --- /dev/null +++ b/checker.py @@ -0,0 +1,334 @@ +from threading import Thread +from mysql.connector import connect, Error +from time import sleep, time +from asics import ASICs +from macros import * +import os, re, json, socket, requests + + +class CThread(Thread): + conn = None + + def __init__(self, event): + super(CThread, self).__init__() + self.event = event + + + def getreg(self, reg, str): + res = re.findall(reg, str) + if res: return res[0][1] + else: return False + + + def getstat_AntminerL3(self, log): + status = 'ok' + + return status + + + def getstat_Avalon1XXX(self, log, type): + status = 'ok' + + rej = float(self.getreg('("Device Rejected%":([0-9.]+),)', log)) + ghs = int(float(self.getreg('(GHSmm\[([0-9.:,]+)\])', log))) + avg = int(float(self.getreg('(GHSavg\[([0-9.: ]+)\])', log))) +# brd = self.getreg('(MGHS\[([0-9.:, ]+)\])', log) + tmp = int(self.getreg('(TMax\[([0-9]+)\])', log)) + + chash = CONF.get('asics', ASICs(type).name, 'crit', 'hash') + whash = CONF.get('asics', ASICs(type).name, 'warn', 'hash') + + ctemp = CONF.get('asics', ASICs(type).name, 'crit', 'temp') + wtemp = CONF.get('asics', ASICs(type).name, 'warn', 'temp') + + crej = CONF.get('asics', ASICs(type).name, 'crit', 'rej') + wrej = CONF.get('asics', ASICs(type).name, 'warn', 'rej') + + if rej > crej or ghs < chash or avg < chash or tmp > ctemp: status = 'crit' + elif rej > wrej or ghs < whash or avg < whash or tmp > wtemp: status = 'warn' + + DEBUG(f"{rej} | {ghs} | {avg} | {brd} | {tmp}") + + return status + + + def getstat_AntminerS19(self, log): + status = 'ok' + + rej = float(self.getreg('("Device Rejected%": ([0-9.]+),)', log)) + ghs = int(float(self.getreg('("GHS 5s": ([0-9.]+),)', log))) + avg = int(float(self.getreg('("GHS av": ([0-9.]+),)', log))) + tmp = int(self.getreg('("TMax": ([0-9]+),)', log)) + + chash = CONF.get('asics', ASICs(type).name, 'crit', 'hash') + whash = CONF.get('asics', ASICs(type).name, 'warn', 'hash') + + ctemp = CONF.get('asics', ASICs(type).name, 'crit', 'temp') + wtemp = CONF.get('asics', ASICs(type).name, 'warn', 'temp') + + crej = CONF.get('asics', ASICs(type).name, 'crit', 'rej') + wrej = CONF.get('asics', ASICs(type).name, 'warn', 'rej') + + if rej > crej or ghs < chash or avg < chash or tmp > ctemp: status = 'crit' + elif rej > wrej or ghs < whash or avg < whash or tmp > wtemp: status = 'warn' + + DEBUG(f"{rej} | {ghs} | {avg} | {brd} | {tmp}") + + return status + + + def gettype(self, ip): + info = self.rtcp(ip, 4028, '{"command": "version"}') + + if not info: + return False + + if CONF.get('debug') and info: + info1 = self.rtcp(ip, 4028, '{"command": "stats"}') + info2 = self.rtcp(ip, 4028, '{"command": "version"}') + info3 = self.rtcp(ip, 4028, '{"command": "summary"}') + info4 = self.rtcp(ip, 4028, '{"command": "pools"}') + info5 = self.rtcp(ip, 4028, '{"command": "devdetails"}') + info6 = self.rtcp(ip, 4028, '{"command": "get_version"}') + info7 = self.rtcp(ip, 4028, '{"command": "status"}') + info8 = self.rtcp(ip, 4028, '{"command": "get_miner_info"}') + info9 = self.rtcp(ip, 4028, '{"command": "devs"}') + info10 = self.rtcp(ip, 4028, '{"command": "notify"}') + info11 = self.rtcp(ip, 4028, '{"command": "coin"}') + info12 = self.rtcp(ip, 4028, '{"command": "edevs"}') + info13 = self.rtcp(ip, 4028, '{"command": "estats"}') + + DEBUG(f"[{ip}] {info1}") + DEBUG(f"[{ip}] {info2}") + DEBUG(f"[{ip}] {info3}") + DEBUG(f"[{ip}] {info4}") + DEBUG(f"[{ip}] {info5}") + DEBUG(f"[{ip}] {info6}") + DEBUG(f"[{ip}] {info7}") + DEBUG(f"[{ip}] {info8}") + DEBUG(f"[{ip}] {info9}") + DEBUG(f"[{ip}] {info10}") + DEBUG(f"[{ip}] {info11}") + DEBUG(f"[{ip}] {info12}") + DEBUG(f"[{ip}] {info13}") + + info += self.rtcp(ip, 4028, '{"command": "devdetails"}') + + # Bitmain + if "Antminer" in info: + if "S19J Pro" in info: + return ASICs.AntminerS19JPro.value + if "S19J" in info: + return ASICs.AntminerS19J.value + if "S19 Pro" in info: + return ASICs.AntminerS19Pro.value + if "S19" in info: + return ASICs.AntminerS19.value + + if "L3+" in info: + return ASICs.AntminerL3plus.value + if "L7" in info: + return ASICs.AntminerL3plus.value + + if "T17+" in info: + return ASICs.AntminerT17plus.value + if "T17" in info: + return ASICs.AntminerT17.value + + # Avalon + if "Avalon" in info: + if "1066" in info: + return ASICs.Avalon1066.value + if "1126" in info: + return ASICs.Avalon1126.value + + # WhatsMiner + if "bitmicro" in info: + if "M20s": + return ASICs.WhatsMinerM20s.value + if "M21s": + return ASICs.WhatsMinerM21s.value + if "M30": + return ASICs.WhatsMinerM30.value + if "M31": + return ASICs.WhatsMinerM31.value + if "M50": + return ASICs.WhatsMinerM50.value + + # Innosilicon + if False: + pass + + return False + + + def rtcp(self, ip, port, cmd): + cmd = cmd.encode('utf-8') + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.connect((ip, port)) + s.sendall(cmd) + tdata = [] + while 1: + data = s.recv(1024*32) + if not data: break + tdata.append(data) + + data = b''.join(tdata).decode("utf-8") + + return data + except OSError as e: + # ERR(e) + return False + + + def check_AntminerL3(self, ip): +# f'http://{ip}/cgi-bin/get_miner_status.cgi' +# f'http://{ip}/cgi-bin/get_miner_conf.cgi' + summ = self.rtcp(ip, 4028, "summary") + stat = self.rtcp(ip, 4028, "estats") + info = self.rtcp(ip, 4028, "version") + + if summ and stat: + mac = json.loads(requests.get( + f'http://{ip}/cgi-bin/get_system_info.cgi', + auth=requests.auth.HTTPDigestAuth('root', 'root')).text)['macaddr'].lower() + + log = f"{info} ||| {summ} ||| {stat}" + + return log, mac + + return False, False + + + def check_AntminerS19(self, ip): +# f'http://{ip}/cgi-bin/get_miner_status.cgi' +# f'http://{ip}/cgi-bin/get_miner_conf.cgi' + summ = self.rtcp(ip, 4028, "summary") + stat = self.rtcp(ip, 4028, "stats") + info = self.rtcp(ip, 4028, "version") + + if summ and stat: + mac = json.loads(requests.get( + f'http://{ip}/cgi-bin/get_network_info.cgi', + auth=requests.auth.HTTPDigestAuth('root', 'root')).text)['macaddr'].lower() + + log = f"{info} ||| {summ} ||| {stat}" + + return log, mac + + return False, False + + + def check_Avalon1XXX(self, ip): + summ = self.rtcp(ip, 4028, "summary") + stat = self.rtcp(ip, 4028, "estats") + info = self.rtcp(ip, 4028, "version") + + if summ and stat: + mac = self.getreg(r'(MAC=([A-z0-9]+))', info) + if mac: mac = ':'.join(mac[i:i+2] for i in range(0,12,2)) + else: mac = ':' + + log = f"{info} ||| {summ} ||| {stat}" + + return log, mac + + return False, False + + + def check(self, ip): + try: + type = self.gettype(ip) + + if not type: + return (False, False, False, False, False), False + + if type in [ASICs.AntminerL3plus.value]: + log, mac = self.check_AntminerL3(ip) + status = self.getstat_AntminerL3(log, type) + elif type in [ASICs.Avalon1066.value, ASICs.Avalon1126.value]: + log, mac = self.check_Avalon1XXX(ip) + status = self.getstat_Avalon1XXX(log, type) + elif type in [ASICs.AntminerS19.value, ASICs.AntminerS19J.value, ASICs.AntminerS19Pro.value, ASICs.AntminerS19JPro.value]: + log, mac = self.check_AntminerS19(ip) + status = self.getstat_AntminerS19(log, type) + + return (ip, mac, type, int(time()), log), status + except Exception as e: + WARN('Minor error in Thread: ' + str(e)) + return (False, False, False, False, False), False + + + def checkall(self): + ips = [] + + with self.conn.cursor(dictionary=True) as c: + c.execute("SELECT * FROM `ips` WHERE `location` = %s", (CONF.get('location'),)) + + for ip in c.fetchall(): + ip = ip['ip'] + if ip.isalpha() or not ip.find('-'): + ips.append(ip) + else: + i_p = ip.split('.') + i_p_new = [] + for i in i_p: + i_p_new.append(i.split('-')) + i_p_new[-1][0] = int(i_p_new[-1][0]) + i_p_new[-1][-1] = int(i_p_new[-1][-1]) + + for i1 in range(i_p_new[0][0], i_p_new[0][-1] + 1): + for i2 in range(i_p_new[1][0], i_p_new[1][-1] + 1): + for i3 in range(i_p_new[2][0], i_p_new[2][-1] + 1): + for i4 in range(i_p_new[3][0], i_p_new[3][-1] + 1): + ips.append(f"{i1}.{i2}.{i3}.{i4}") + + Tca = [] + ca = [] + for ip in ips: + T = Thread(target=lambda x: ca.append(self.check(x)), args=(ip,)) + Tca.append(T) + for i in range(len(Tca)): + Tca[i].start() + for i in range(len(Tca)): + Tca[i].join() + + with self.conn.cursor() as c: + c.execute(f"DELETE FROM `laststate` WHERE `location` = %s", (CONF.get('location'),)) + + for i in ca: + i, status = i + if i[4]: + c.execute(f"INSERT INTO `asiclogs` (`ip`, `mac`, `type`, `time`, `log`) VALUES (%s, %s, %s, %s, %s)", i) + + if i[1] == ':': + continue + c.execute( + f"INSERT INTO `laststate` (`ip`, `mac`, `type`, `location`, `status`, `time`) VALUES (%s, %s, %s, %s, %s, %s)", + (i[0], i[1], i[2], CONF.get('location'), status, int(time()))) + + self.conn.commit() + + + def run(self): + try: + SUCC("Checker started!") + sleep(5) + + self.conn = connect( + host=CONF.get('db', 'host'), + user=CONF.get('db', 'user'), + password=CONF.get('db', 'password'), + database=CONF.get('db', 'name')) + + while 1: + if self.event.is_set(): + SUCC("Checker stopped!") + break + + self.checkall() + sleep(10) + except Exception as e: + CRIT(str(e)) + os._exit(1) diff --git a/checkmem b/checkmem new file mode 100755 index 0000000..b56a9f2 --- /dev/null +++ b/checkmem @@ -0,0 +1,3 @@ +#!/bin/bash + +watch -n 0.1 'ps -o vsize,command,pcpu ax | sort -b -k3 -r | grep -E -i -w "[0-9]+ python"' diff --git a/cleaner.py b/cleaner.py new file mode 100644 index 0000000..bf34688 --- /dev/null +++ b/cleaner.py @@ -0,0 +1,43 @@ +from threading import Thread +from mysql.connector import connect, Error +from time import sleep, time +from macros import * +import os + + +class NThread(Thread): + conn = None + + def __init__(self, event): + super(NThread, self).__init__() + self.event = event + + + def clean(self): + with self.conn.cursor() as c: + c.execute(f"DELETE FROM `asiclogs` WHERE `time` < UNIX_TIMESTAMP() - %s", (CONF.get('db-log-days'),)) + c.execute(f"DELETE FROM `laststate` WHERE `time` < UNIX_TIMESTAMP() - 30") + + self.conn.commit() + + + def run(self): + try: + SUCC("Cleaner started!") + + self.conn = connect( + host=CONF.get('db', 'host'), + user=CONF.get('db', 'user'), + password=CONF.get('db', 'password'), + database=CONF.get('db', 'name')) + + while 1: + if self.event.is_set(): + SUCC('Cleaner stopped!') + break + + self.clean() + sleep(5) + except Exception as e: + CRIT(str(e)) + os._exit(1) diff --git a/colors.py b/colors.py new file mode 100644 index 0000000..0dd731a --- /dev/null +++ b/colors.py @@ -0,0 +1,20 @@ +RESET = "\033[0m" +BOLD = "\033[1m" + +BLACK = "\033[30m" +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +MAGENTA = "\033[35m" +CYAN = "\033[36m" +WHITE = "\033[37m" + +BGBLACK = "\033[40m" +BGRED = "\033[41m" +BGGREEN = "\033[42m" +BGYELLOW = "\033[43m" +BGBLUE = "\033[44m" +BGMAGENTA = "\033[45m" +BGCYAN = "\033[46m" +BGWHITE = "\033[47m" diff --git a/config.json b/config.json new file mode 100644 index 0000000..4ed3506 --- /dev/null +++ b/config.json @@ -0,0 +1,73 @@ +{ + "license": "00001111-2222-3333-4444-555566667777", + "location": "Underground", + "logging": true, + "db-log-days": 7, + "debug": true, + "updates": false, + "port": 9070, + "telegram": { + "enable": true, + "channel": -1001871940722, + "token": "5953600362:AAELgZr0Ldstf0omK43zKrAzGXOuMGowcf8", + "normal-notify": true, + "warn-notify": false, + "crit-notify": true + }, + "db": { + "host": "127.0.0.1", + "user": "root", + "password": "0", + "name": "myasics" + }, + "asics": { + "L3plus": { + "crit": { + "hash": 40000, + "temp": 90, + "rejects": 2.5 + }, + "warn": { + "hash": 45000, + "temp": 80, + "rejects": 1.5 + } + }, + "S19": { + "crit": { + "hash": 80000, + "temp": 90, + "rejects": 2.5 + }, + "warn": { + "hash": 85000, + "temp": 80, + "rejects": 1.5 + } + }, + "Avalon1066": { + "crit": { + "hash": 42000, + "temp": 90, + "rejects": 2.5 + }, + "warn": { + "hash": 46000, + "temp": 80, + "rejects": 1.5 + } + }, + "Avalon1126": { + "crit": { + "hash": 64000, + "temp": 90, + "rejects": 2.5 + }, + "warn": { + "hash": 66000, + "temp": 80, + "rejects": 1.5 + } + } + } +} diff --git a/config.py b/config.py new file mode 100644 index 0000000..92b266c --- /dev/null +++ b/config.py @@ -0,0 +1,3 @@ +APPNAME='Bit.ASICmon' +VERSION='v0.1.1a' +DEBUGGING=True diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..147ba02 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.1' + +services: + + db: + image: mysql + command: --default-authentication-plugin=mysql_native_password + restart: always + environment: + MYSQL_ROOT_PASSWORD: 0 + ports: + - 3306:3306 diff --git a/index.js b/index.js new file mode 100644 index 0000000..cb8359f --- /dev/null +++ b/index.js @@ -0,0 +1,223 @@ +const sprintf = (...[string, ...args]) => { + return string.replace(/{(\d+)}/g, (match, number) => args[number] ?? match); +} + +const getJSON = async url => { + const resp = await fetch(url) + const json = await resp.json() + return json; +} + +const createPopup = async id => { + const asics = await getJSON('/api/asictypes') + + asicoptions = '' + asics.forEach(elem => { + asicoptions += sprintf('', elem['key'], elem['value']) + }) + + document.querySelector('body').innerHTML += sprintf(popup, id, asicoptions, 'content') +} + +const deletePopup = id => { + document.querySelector('#popup-' + id).remove() +} + +const update = async () => { + let inner = await getStats() + document.querySelector('.grid').innerHTML = inner +} + +const datafn = async () => { + // return await getJSON('/api/webinit') +} + +const data = datafn() + +console.log(data) + +const cols = 40 +const rows = 40 + + +const css = + ` + ` + +const header = + '
' + + '
' + + 'Всего устройств: {0}, Предупреждений, {1}, Ошибки: {2}, В сети: {3}, Не в сети: {4}' + + '
' + + '
' + + +const cell = + '
' + + '{2}' + + '
' + +const grid = + '
' + + '{0}' + + '
' + +const popup = + '' + +const getStats = async () => { + let cells = '' + let info = await getJSON('/curstatus.json') + for(let i = 1; i <= cols * rows; i++) { + if(info.asics[i]?.status == "ok") + cells += sprintf(cell, i, 'green', info.asics[i]?.hashrate) + else if(info.asics[i]?.status == "warn") + cells += sprintf(cell, i, 'yellow', info.asics[i]?.hashrate) + else if(info.asics[i]?.status == "crit") + cells += sprintf(cell, i, 'red', info.asics[i]?.hashrate) + else if(info.asics[i]?.status == "off") + cells += sprintf(cell, i, 'darkred', '') + else + cells += sprintf(cell, i, '', '') + } + return cells +} + +const run = async () => { + document.querySelector('body').innerHTML += css + document.querySelector('body').innerHTML += sprintf(header) + + let inner = sprintf(grid, await getStats()) + document.querySelector('body').innerHTML += inner + + setInterval(async () => { + await update() + }, 10000) +} + + +run() diff --git a/license.py b/license.py new file mode 100644 index 0000000..6e3a977 --- /dev/null +++ b/license.py @@ -0,0 +1,76 @@ +from threading import Thread +from time import sleep, time +from macros import * +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from hashlib import sha256 +from base64 import b64decode +from datetime import datetime +from urllib import request +import os, json, requests + + +class LThread(Thread): + _FIRST = True + + + def __init__(self, event): + super(LThread, self).__init__() + self.event = event + + + def check(self): + url = 'https://license.bitheaven.ru/api/v1/license.check' + post = { + 'key': CONF.get('license'), + 'loc': CONF.get('location'), + 'time': int(time()), + 'data': get_random_bytes(16) + } + curtime = post['time'] + + try: + req = requests.post(url, data=post).json() + except requests.exceptions.RequestException as e: + return False + + if req['error']: + ERR(f"Server error! {req['msg']}") + return True + + key = sha256(post['loc'].encode('utf-8')).hexdigest()[0:32].encode("utf-8") + iv = post['data'] + aes = AES.new(key, AES.MODE_OFB, iv=iv) + msg = aes.decrypt(b64decode(req['data'])).decode('unicode_escape').strip() + + until = datetime.fromtimestamp(req['until']).strftime('%d.%m.%Y') + + if self._FIRST: + INFO(f'License paid until: {until}') + self._FIRST = False + + return f"{CONF.get('license')}{curtime}" == msg + + + def run(self): + try: + while 1: + if self.event.is_set(): + break + + cur = self.check() + + if not cur: + try: + request.urlopen('http://1.1.1.1', timeout=2) + CRIT("License wrong or expired!") + WARN("Get it on https://bhev.ru/glfbam") + sleep(60) + except: + ERR("No access to network!") +# os._exit(1) + + sleep(5) + except Exception as e: + CRIT(str(e)) + os._exit(1) diff --git a/log/.2023-05-10.log.swp b/log/.2023-05-10.log.swp new file mode 100644 index 0000000..8b370b2 Binary files /dev/null and b/log/.2023-05-10.log.swp differ diff --git a/macros.py b/macros.py new file mode 100644 index 0000000..ec3c88c --- /dev/null +++ b/macros.py @@ -0,0 +1,97 @@ +from datetime import datetime +from colors import * +from platform import system +import inspect +import json, os, config + + +__DEBUGGING_ALERT = False + +MUSTDIE = system().lower() == 'windows' +LINUX = system().lower() == 'linux' +MACOS = system().lower() == 'darwin' + + +class CONF(): + def __init__(self): + CONF.config = json.load(open('config.json')) + + + @staticmethod + def get(*params): + conf = CONF.config + + for param in params: + if param in conf.keys(): + conf = conf[param] + + if conf: + return conf + + return False + +CONF() + + +def __GET_TIME(): + return datetime.now().strftime("%H:%M:%S.%f") + + +def __GET_DATE(): + return datetime.now().strftime("%Y-%m-%d") + + +def __ADD_TO_LOG(str): + if CONF.get('logging'): + if not os.path.isdir('log'): + os.mkdir('log') + log = open(f'log/{__GET_DATE()}.log', 'a') + log.write(str + '\n') + log.close() + + +def __PRINT_LOG(str, log): + if CONF.get('debug') and config.DEBUGGING: + st = inspect.stack()[2] + caller = st.filename.split('/')[-1].split('.')[0] + callerline = st.lineno + str = f'[{__GET_TIME()}] [{caller}:{callerline}] {str}' + log = f'[{caller}:{callerline}] {log}' + else: + str = f'[{__GET_TIME()}] {str}' + + print(str) + __ADD_TO_LOG(log) + + +def INFO(s = ''): + __PRINT_LOG(f"[INFO] {str(s)}{RESET}", str(s)) + + +def DEBUG(s = ''): + global __DEBUGGING_ALERT + + if not CONF.get('debug'): + return None + + if config.DEBUGGING: + __PRINT_LOG(f"{MAGENTA}[DEBUG]{RESET} {BGBLUE}{YELLOW}{BOLD}{str(s)}{RESET}", str(s)) + elif not __DEBUGGING_ALERT: + WARN('DEBUGGING DISABLED BY OWNER!') + __DEBUGGING_ALERT = True + + +def SUCC(s = ''): + __PRINT_LOG(f"{GREEN}[SUCCESS]{RESET} {GREEN}{str(s)}{RESET}", str(s)) + + +def WARN(s = ''): + __PRINT_LOG(f"{YELLOW}[WARN]{RESET} {YELLOW}{str(s)}{RESET}", str(s)) + + +def ERR(s = ''): + __PRINT_LOG(f"{RED}[ERROR]{RESET} {RED}{str(s)}{RESET}", str(s)) + + +def CRIT(s = ''): + __PRINT_LOG(f"{RED}{BOLD}[CRITICAL]{RESET} {BGRED}{WHITE} {str(s)} {RESET}", str(s)) diff --git a/main.py b/main.py new file mode 100755 index 0000000..8e76e1c --- /dev/null +++ b/main.py @@ -0,0 +1,95 @@ +#!/usr/bin/python + +from updater import UThread +from license import LThread +from checker import CThread +from cleaner import NThread +from telegrambot import TGThread +from webserver import WSThread +from time import sleep +from macros import * +from colors import * +from threading import Event +import config, sys, os, asyncio + + +INFO('╔' + '═'*(len(config.APPNAME) + len(config.VERSION) + 3) + '╗') +INFO(f"║ {CYAN}{BOLD}{config.APPNAME}{RESET} {config.VERSION} ║") +INFO('╚' + '═'*(len(config.APPNAME) + len(config.VERSION) + 3) + '╝') + +if CONF.get('debug'): + INFO('DEBUG TEST START ' + '*' * 20) + DEBUG(f"{RESET}{BLACK}*{RED}*{GREEN}*{YELLOW}*{BLUE}*{MAGENTA}*{CYAN}*{WHITE}*") + DEBUG(f"{RESET}{BOLD}{BLACK}*{RED}*{GREEN}*{YELLOW}*{BLUE}*{MAGENTA}*{CYAN}*{WHITE}*") + DEBUG(f"{RESET}{BGBLACK}*{BGRED}*{BGGREEN}*{BGYELLOW}*{BGBLUE}*{BGMAGENTA}*{BGCYAN}*{BGWHITE}*") + INFO('Test message') + DEBUG('Test message') + SUCC('Test message') + WARN('Test message') + ERR('Test message') + CRIT('Test message') + INFO('DEBUG TEST END ' + '*' * 22 + RESET) + + +eupdate = Event() +estop = Event() + +lt = LThread(eupdate) +lt.daemon = True +lt.start() + +wt = WSThread(eupdate) +wt.daemon = True +wt.start() + +ct = CThread(eupdate) +ct.daemon = True +ct.start() + +nt = NThread(eupdate) +nt.daemon = True +nt.start() + +tt = TGThread(eupdate, estop) +tt.daemon = True +tt.start() + +ut = UThread() +ut.daemon = True +ut.start() + +updated = False + +try: + while 1: + if lt.is_alive() and wt.is_alive() and ct.is_alive() and tt.is_alive() and ut.is_alive() and nt.is_alive(): + sleep(1) + elif lt.is_alive() and wt.is_alive() and ct.is_alive() and tt.is_alive() and nt.is_alive(): + eupdate.set() + if lt.is_alive(): lt.join() + if wt.is_alive(): wt.join() + if ct.is_alive(): ct.join() + if nt.is_alive(): nt.join() + if tt.is_alive(): tt.join() + updated = True + SUCC('Program updated, reboot!') + break + else: + estop.set() + sleep(1) + CRIT('Program broken.') + os._exit(3) +except KeyboardInterrupt: + sys.stdout.flush() + print('\r', end='') + estop.set() + sleep(1) + CRIT('STOP') +except Exception as e: + CRIT(str(e)) + estop.set() + sleep(1) + os._exit(1) + +if updated: + os.execl(sys.argv[0], *sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5c9182b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +python-telegram-bot +pyinstaller +pyarmor<8.0.0 diff --git a/telegrambot.py b/telegrambot.py new file mode 100644 index 0000000..b9adab9 --- /dev/null +++ b/telegrambot.py @@ -0,0 +1,125 @@ +from threading import Thread +from telegram import Bot, error +from asics import ASICs +from time import sleep +from mysql.connector import connect, Error +from macros import * +import os, json, asyncio + + +class TGThread(Thread): + conn = None + bot = None + asics = {} + + + def __init__(self, event, event2): + super(TGThread, self).__init__() + self.event = event + self.event2 = event2 + + + async def sendmsg(self, msg): + return await self.bot.send_message(CONF.get('telegram', 'channel'), msg, 'MarkdownV2') + + + async def check(self): + msg = [] + + for i in self.asics.keys(): + self.asics[i]['tooff'] -= 1 + + with self.conn.cursor(dictionary=True) as c: + c.execute(f"SELECT * FROM `laststate`") + + for i in c.fetchall(): + if not i['mac'] in self.asics.keys(): + self.asics[i['mac']] = {} + self.asics[i['mac']]['alert'] = 0 + + msg.append(f"🟢 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` online\!") + + self.asics[i['mac']]['tooff'] = 3 + self.asics[i['mac']]['status'] = i['status'] + + if i['status'] == 'crit': + if CONF.get('telegram', 'crit-notify') and self.asics[i['mac']]['alert'] < 2: + msg.append(f"🔴 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` crit status\!") + self.asics[i['mac']]['alert'] = 2 + elif i['status'] == 'warn': + if CONF.get('telegram', 'warn-notify') and self.asics[i['mac']]['alert'] < 1: + msg.append(f"🟡 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` warn status\!") + self.asics[i['mac']]['alert'] = 1 + elif i['status'] == 'ok': + if CONF.get('telegram', 'normal-notify') and self.asics[i['mac']]['alert'] != 0: + msg.append(f"🟢 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` normal status\!") + self.asics[i['mac']]['alert'] = 0 + + if self.asics[i['mac']]['tooff'] <= 0: + msg.append(f"🔴 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` offline\!") + del self.asics[i['mac']] + + if len(msg) > 0: + await self.sendmsg('\n'.join(msg)) + + + async def runbot(self): + try: + res = await self.sendmsg('🟢 *Notify server online\!*') + INFO(f"Telegram channel ID: {res['chat']['id']}") + + while 1: + if self.event.is_set(): + SUCC('Telegram stopped!') + await self.sendmsg('🟡 Updating software\! Recommend check all running instances.') + break + + await self.check() + await asyncio.sleep(15) + + except error.Forbidden as e: + WARN(str(e)) + except error.TimedOut as e: + WARN(str(e)) + except Exception as e: + CRIT(str(e)) +# os._exit(1) + + + async def stopalert(self): + while 1: + if self.event2.is_set(): + await self.sendmsg('🔴 *Notify server offline\!*') + + await asyncio.sleep(0.5) + + + async def main(self): + await asyncio.gather(self.stopalert(), self.runbot()) + + + def run(self): + try: + if CONF.get('telegram', 'enable'): + SUCC('Telegram started!') + + self.conn = connect( + host=CONF.get('db', 'host'), + user=CONF.get('db', 'user'), + password=CONF.get('db', 'password'), + database=CONF.get('db', 'name')) + self.conn.autocommit = True + + self.bot = Bot(CONF.get('telegram', 'token')) + + asyncio.run(self.main()) + else: + while 1: + if self.event.is_set(): + SUCC('Telegram stopped!') + break + + sleep(5) + except Exception as e: + CRIT(str(e)) + os._exit(1) diff --git a/updater.py b/updater.py new file mode 100644 index 0000000..5cd6ea5 --- /dev/null +++ b/updater.py @@ -0,0 +1,54 @@ +from threading import Thread +from macros import * +from config import * +from time import sleep +import urllib, os, requests + +class UThread(Thread): + path = os.path.dirname(__file__) + + def __init__(self): + super(UThread, self).__init__() + + + def run(self): + global VERSION + + try: + SUCC('Updater started!') + while 1: + if not CONF.get('updates'): + sleep(1) + continue + + version = requests.get('https://mirror.bitheaven.ru/main/versions/Bit.ASICmon-a').text + + if version == VERSION: + sleep(300) + continue + + INFO('Update found! Downloading...') + if LINUX: + with urllib.request.urlopen("https://mirror.bitheaven.ru/main/archive/Bit.ASICmon-a_linux") as upd: + with open(self.path, "wb+") as f: + INFO('Installing update...') + f.write(upd.read()) + INFO('Stopping process...') + break + elif MACOS: + with urllib.request.urlopen("https://mirror.bitheaven.ru/main/archive/Bit.ASICmon-a_macos") as upd: + with open(self.path, "wb+") as f: + INFO('Installing update...') + f.write(upd.read()) + INFO('Stopping process...') + break + elif MUSTDIE: + with urllib.request.urlopen("https://mirror.bitheaven.ru/main/archive/Bit.ASICmon-a_mustdie.exe") as upd: + with open(self.path, "wb+") as f: + INFO('Installing update...') + f.write(upd.read()) + INFO('Stopping process...') + break + except Exception as e: + CRIT(str(e)) + os._exit(1) diff --git a/webserver.py b/webserver.py new file mode 100644 index 0000000..afdf8c4 --- /dev/null +++ b/webserver.py @@ -0,0 +1,107 @@ +from flask import Flask, send_file, request +from time import sleep +from werkzeug.serving import make_server +from threading import Thread +from mysql.connector import connect, Error +from macros import * +import os, json, config, requests, logging + + +class WSThread(Thread): + app = Flask(__name__) + conn = None + + def __init__(self, event): + super(WSThread, self).__init__() + self.event = event + + log = logging.getLogger('werkzeug') + log.disabled = True + + + @app.route('/') + @app.route('/index') + def index(): + return '' + + + @app.route('/index.js') + def indexjs(): + return send_file('index.js') + + + @app.route('/curstatus.json') + def curstatus(): + j = {} + j['info'] = {} + j['info']['asics'] = {} + + return json.dumps(j), 200, {'ContentType':'application/json'} + + + def dbinit(self): + with self.conn.cursor() as c: + c.execute(""" + CREATE TABLE IF NOT EXISTS `ips` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `ip` VARCHAR(64) NOT NULL, + `location` VARCHAR(64) NOT NULL + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS `laststate` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `ip` VARCHAR(64) NOT NULL, + `mac` VARCHAR(32) NOT NULL, + `type` INT(11) NOT NULL, + `location` VARCHAR(64) NOT NULL, + `status` VARCHAR(32) NOT NULL, + `time` INT(11) NOT NULL + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS `asiclogs` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `ip` VARCHAR(64) NOT NULL, + `mac` VARCHAR(64) NOT NULL, + `type` VARCHAR(64) NOT NULL, + `time` INT(11) NOT NULL, + `log` TEXT NOT NULL + ) + """) + + self.conn.commit() + + + def runweb(self): + self.server = make_server('0.0.0.0', CONF.get('port'), self.app) + self.server.serve_forever() + + + def run(self): + try: + with connect(host=CONF.get('db', 'host'), user=CONF.get('db', 'user'), password=CONF.get('db', 'password')) as conn: + with conn.cursor() as c: + c.execute(f"CREATE DATABASE IF NOT EXISTS {CONF.get('db', 'name')}") + + self.conn = connect( + host=CONF.get('db', 'host'), + user=CONF.get('db', 'user'), + password=CONF.get('db', 'password'), + database=CONF.get('db', 'name')) + self.dbinit() + + web = Thread(target=self.runweb) + web.daemon = True + web.start() + + SUCC(f"Web interface started at port {CONF.get('port')}!") + + while not self.event.is_set(): + sleep(1) + + SUCC(f"Web server stopped!") + self.server.shutdown() + except Exception as e: + CRIT(str(e)) + os._exit(1)