From 553754cd776253fc6aaf21a470f51ac9ad6ac6f3 Mon Sep 17 00:00:00 2001 From: BitHeaven Date: Mon, 25 Mar 2024 09:32:11 +0500 Subject: [PATCH] Wipe branch --- .gitignore | 162 +++++++++++++++++++ LICENSE | 21 +++ README.md | 4 + TODO | 3 + asics.py | 27 ++++ build.sh | 4 + checker.py | 334 ++++++++++++++++++++++++++++++++++++++++ checkmem | 3 + cleaner.py | 43 ++++++ colors.py | 20 +++ config.json | 73 +++++++++ config.py | 3 + docker-compose.yml | 12 ++ index.js | 223 +++++++++++++++++++++++++++ license.py | 76 +++++++++ log/.2023-05-10.log.swp | Bin 0 -> 1024 bytes macros.py | 97 ++++++++++++ main.py | 95 ++++++++++++ requirements.txt | 4 + telegrambot.py | 125 +++++++++++++++ updater.py | 54 +++++++ webserver.py | 107 +++++++++++++ 22 files changed, 1490 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TODO create mode 100644 asics.py create mode 100755 build.sh create mode 100644 checker.py create mode 100755 checkmem create mode 100644 cleaner.py create mode 100644 colors.py create mode 100644 config.json create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 index.js create mode 100644 license.py create mode 100644 log/.2023-05-10.log.swp create mode 100644 macros.py create mode 100755 main.py create mode 100644 requirements.txt create mode 100644 telegrambot.py create mode 100644 updater.py create mode 100644 webserver.py 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 0000000000000000000000000000000000000000..8b370b209d402eaa46e44d0b75c327344c6d200b GIT binary patch literal 1024 zcmYc?$V<%2S1{8vVn6{BnN|!*nI##iiDjvIC^DFwoc!d(oQ(Y965V`ENhFb+{B(UI c10!Qy15;f?13e%YNph4q8UmvsK=%*;08PmdlK=n! literal 0 HcmV?d00001 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)