From c6979addd0f022b89604661d3719698a6c8716c9 Mon Sep 17 00:00:00 2001
From: ELForcer
Date: Thu, 29 Feb 2024 01:04:07 +0400
Subject: [PATCH] Updates
---
.vscode/settings.json | 5 +
app/__init__.py | 41 +-
app/pages/rustdesk/config.ini | 29 +
app/pages/rustdesk/script.js | 41 +
app/pages/rustdesk/source.py | 61 +
app/pages/rustdesk/template.htm | 84 ++
app/source/API_App.py | 32 +
app/source/API_Common.py | 1176 +++++++++++++++++++
app/source/OLD_API_Common.py | 1084 +++++++++++++++++
app/source/OLD_Pages.py | 138 +++
app/source/Pages.py | 127 ++
app/static/image/rustdesk/rd1.png | Bin 42723 -> 60601 bytes
app/static/image/rustdesk/rd2.png | Bin 54182 -> 66663 bytes
app/static/image/rustdesk/rd3.png | Bin 18140 -> 62302 bytes
app/static/image/rustdesk/rd4.png | Bin 11593 -> 33045 bytes
app/static/image/rustdesk/rd5.png | Bin 0 -> 63251 bytes
app/static/image/rustdesk/rd6.png | Bin 0 -> 49733 bytes
app/static/image/rustdesk/rd8.png | Bin 0 -> 25204 bytes
app/static/image/rustdesk/rd9.png | Bin 0 -> 23740 bytes
app/templates/block_head.htm | 18 +
app/templates/{_header => block_header.htm} | 0
app/templates/error_403.htm | 17 +
app/templates/error_404.htm | 17 +
app/templates/error_500.htm | 17 +
app/templates/rustdesk.htm | 118 --
app/views.py | 52 +-
shell/1a-CheckSystem.sh | 18 +-
shell/r.txt | 2 +-
28 files changed, 2897 insertions(+), 180 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 app/pages/rustdesk/config.ini
create mode 100644 app/pages/rustdesk/script.js
create mode 100755 app/pages/rustdesk/source.py
create mode 100644 app/pages/rustdesk/template.htm
create mode 100644 app/source/API_App.py
create mode 100644 app/source/API_Common.py
create mode 100644 app/source/OLD_API_Common.py
create mode 100644 app/source/OLD_Pages.py
create mode 100644 app/source/Pages.py
create mode 100644 app/static/image/rustdesk/rd5.png
create mode 100644 app/static/image/rustdesk/rd6.png
create mode 100644 app/static/image/rustdesk/rd8.png
create mode 100644 app/static/image/rustdesk/rd9.png
create mode 100644 app/templates/block_head.htm
rename app/templates/{_header => block_header.htm} (100%)
create mode 100644 app/templates/error_403.htm
create mode 100644 app/templates/error_404.htm
create mode 100644 app/templates/error_500.htm
delete mode 100644 app/templates/rustdesk.htm
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..17c416b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "python.analysis.extraPaths": [
+ "./app/source"
+ ]
+}
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
index 0bb69cb..5ea3790 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,28 +1,23 @@
#!env/bin/python3.10
# -*- coding: UTF-8 -*-
-#активация FLASK
-#from flask import Flask
+# активация FLASK
+from flask import Flask, send_from_directory
-
-from flask import Flask, request, send_from_directory
-
-from flask import jsonify #для генерации JSON, не актуально
+from flask import jsonify # для генерации JSON, не актуально
from flask_cors import CORS
-#from flask import redirect, url_for
+# from flask import redirect, url_for
-#Активируем сжатие данных
+# Активируем сжатие данных
from flask_compress import Compress
compress = Compress()
app = Flask(__name__, static_url_path='', static_folder='')
compress.init_app(app)
-
-
-#Статика
+# Статика
@app.route('/favicon.ico')
def favicon():
@@ -34,29 +29,29 @@ def favicon():
def send_from_statics(path):
return send_from_directory('static', path)
+
@app.route('/temp/')
def send_from_temp(path):
return send_from_directory('temp', path)
-
+
# enable CORS
CORS(app)
# sanity check route
+
+
@app.route('/ping', methods=['GET'])
def ping_pong():
return jsonify('pong!')
-
-#@app.route('/')
-#def static_file(path):
+
+# @app.route('/')
+# def static_file(path):
# return app.send_static_file(path)#
-
-#активируем файл конфигурации
-app.config.from_object('config')
-#активируем Вьювер
+
+# активируем файл конфигурации
+app.config.from_object('config')
+
+# активируем Вьювер
from app import views
-
-
-
-
diff --git a/app/pages/rustdesk/config.ini b/app/pages/rustdesk/config.ini
new file mode 100644
index 0000000..b0300b7
--- /dev/null
+++ b/app/pages/rustdesk/config.ini
@@ -0,0 +1,29 @@
+[ACCESS]
+# Проверка интерфейса пользователя
+# 1 Администратор
+# 2 Оператор
+# 3 Врач-эксперт
+# 4 Агент
+# 5 Специалист
+# 6 Сортировщик
+# 7 Диспетчер
+# 8 Страховой представитель
+# 9 Сотрудник МФЦ
+# 10 Ведущий специалист
+# Пример: UserInterFace = 1,10 (без пробелов между запятыми)
+UserInterFace = 0
+
+# Минимальный уровень доступа пользователя от 0
+# 0 - без проверки
+# 1 - чтение
+# 2 - чтение и запись
+# 3 - полный доступ
+AccessOMS = 0
+AccessLPU = 0
+AccessUOG = 0
+AccessENP = 0
+AccessSMS = 0
+AccessPostCard = 0
+AccessPhone = 0
+AccessDial = 0
+AccessAnkets = 0
diff --git a/app/pages/rustdesk/script.js b/app/pages/rustdesk/script.js
new file mode 100644
index 0000000..04826ef
--- /dev/null
+++ b/app/pages/rustdesk/script.js
@@ -0,0 +1,41 @@
+// Автор: ELForcer
+// Дата оптимизации: 21.04.2023
+const RootComponent = {
+ data() {
+ return {
+
+ //Цвета
+ M_Red: false,
+ M_Green: false,
+ M_Blue: false,
+ M_Yellow: false,
+
+ //Модальные переменные
+ ModalTitleThread: "",
+ ModalTitle2: "Запуск",
+ ModalBodyText2: "Пожалуйста, подождите...",
+ ModalBodyThread: "",
+ ThreadMessage: "",
+ ThreadVars: {
+ message: '',
+ HTML: "",
+ Started: false,
+ },
+ }; //return
+ }, //data
+
+ //===========================================================================
+ methods: {
+ //-------------------------------------------------------------------------
+
+
+ }, // END metods
+
+ //===========================================================================
+ mounted() {
+ ThreadVars = this.ThreadVars;
+ }, // mounted()
+}; // Vue
+
+//Монтируем Vue как vueapp начиная с тега #vueapp
+vueapp = Vue.createApp(RootComponent).mount("#vueapp");
diff --git a/app/pages/rustdesk/source.py b/app/pages/rustdesk/source.py
new file mode 100755
index 0000000..aedc025
--- /dev/null
+++ b/app/pages/rustdesk/source.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Главное меню
+Дата последней оптимизации: 21.04.2023
+"""
+
+from app import app # чтение из config.py
+from flask import request # получение данных Cookie, GET и POST
+import sys
+import os
+import json
+
+# Подключаем свои библиотеки
+sys.path.append("app/source")
+sys.path.append("app/pages")
+import API_Common as APIC
+
+# Глобальные Переменные
+CurPath: str = "app/pages"
+NameModule = "rustdesk"
+
+
+###############################################################################
+# MAIN
+###############################################################################
+def Main(SessionID):
+
+ # Объявление переменных
+ Var = {"Title": "Главное меню", "ProgName": app.config['PROGNAME']}
+ Modal_Vars = {}
+
+ ###
+ TemplateData = ""
+ ScriptBody = ""
+
+ # Генерируем HTML
+ TemplateName = os.path.join(str(CurPath), str(NameModule), 'template.htm')
+ if (os.path.exists(TemplateName) == True):
+ with open(TemplateName) as fp:
+ TemplateData = fp.read()
+ fp.close()
+
+ # Подгружаем ява скрипт для вставки его в тело шаблона
+ ScriptName = os.path.join(CurPath, NameModule, 'script.js')
+ if (os.path.exists(ScriptName) == True):
+ with open(ScriptName) as fp:
+ ScriptBody = fp.read()
+ fp.close()
+
+ # Подставляем переменные в HTML
+ TemplateData = TemplateData = TemplateData.replace('}}', '}}')
+ TemplateData = TemplateData.replace('{{head}}', APIC.UserHead(Var)) # Подключаем раздел head со скриптами и css
+ TemplateData = TemplateData.replace('{{UserHeader}}', APIC.UserHeader(SessionID, Var)) # Рисуем шапку на странице
+ TemplateData = TemplateData.replace('{{Modals}}', APIC.Modals(Modal_Vars)) # Модальные окна
+ TemplateData = TemplateData.replace('{{ThreadVars_message}}', APIC.ThreadVars_Message()) # Сообщения от потока
+ TemplateData = TemplateData.replace('{{version}}', app.config['VERSION']) # Версия Сервера КМС-ИК
+ TemplateData = TemplateData.replace('{{IPServer}}', request.host.split(":")[0]) # Версия Сервера КМС-ИК
+ TemplateData = TemplateData.replace('{{ScriptBody}}', ScriptBody) # Скрипты. Например, Vue.JS
+
+ return TemplateData # Отправляем обработанный шаблон
diff --git a/app/pages/rustdesk/template.htm b/app/pages/rustdesk/template.htm
new file mode 100644
index 0000000..c7de33d
--- /dev/null
+++ b/app/pages/rustdesk/template.htm
@@ -0,0 +1,84 @@
+
+
+
+
+ {{head}}
+
+
+
+
+ {{UserHeader}}
+
+ 1. Зайти и скачать дистрибутив с сайта RustDesk или скачать:
+
+
+
+ 2. Напротив ID нажать на троеточие;
+ 
+ 3. Выбрать "Сеть"
+ 
+ 4. Выбрать "Нажать "Разблокировать сетевые настройки"
+ 
+
+ Может потребоваться ввод пароля администратора
+ 
+
+
+
+
+
+
+
+
+ 7. Вернитесь в закладку "Главная" и сообщите ваш ID. Дождитесь подключения к Вашему рабочему столу.
+
+ 
+
+
+ Запрос на подключение будет выглядеть так:
+
+ 
+
+
+ Принятое подключение будет выглядеть так:
+
+ 
+
+
+
+
+
diff --git a/app/source/API_App.py b/app/source/API_App.py
new file mode 100644
index 0000000..cf94457
--- /dev/null
+++ b/app/source/API_App.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+"""
+Нестандартные процедуры, которые использует только это приложение
+Дата последней оптимизации: 07.06.2022
+"""
+import uuid # Уникальные имена
+import os # Работа с ОС
+import shutil # Копирование файлов
+import time # работа с временем
+import datetime # Работа с датой
+import sys
+
+from app import app # чтение из config.py
+
+sys.path.append("app/source")
+import API_Common as APIC
+
+
+NameModule = "API_App"
+
+###############################################################################
+def UserHeader(SessionID: str, Vars: dict) -> str:
+ """Вызывается из API_Common"""
+ # print(str(Vars["request"].url.split('/')))
+ request = Vars["request"]
+ CurStr = Vars["CurStr"]
+
+ Link = ""
+ LinkReport = ""
+
+ return CurStr
diff --git a/app/source/API_Common.py b/app/source/API_Common.py
new file mode 100644
index 0000000..701024a
--- /dev/null
+++ b/app/source/API_Common.py
@@ -0,0 +1,1176 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+API с общими функциями и процедурами
+Дата последней оптимизации: 22.05.2022
+Дата обновления: 26.01.2024
+"""
+
+import subprocess
+import uuid # Генератор GUID
+import datetime # Работа с датой
+import time # работа с временем
+import os # Работа с файловой системой
+import requests # Работа с http/s
+
+# Для Криптографии
+import hmac
+import base64
+import hashlib
+
+import socket # Для проверки доступности портов
+from app import app # чтение из config.py
+from flask import request # Для получения метаданных о запросе
+
+import sys # Подключаем свои библиотеки
+sys.path.append("app/source")
+import API_App
+
+appname = app.config['SHMNAME']
+NameModule = "API_Common"
+
+
+################################################################################
+class DataSession:
+ """Класс для передачи данных о сесссии"""
+
+ def __init__(self):
+ self.status: bool = False # Действует сессия или нет
+ self.comment: str = ""
+ self.sessionid: str = ""
+ self.userid: str = ""
+ self.username: str = ""
+ self.stationid: str = ""
+
+
+################################################################################
+def HTML_Progress(Color, IsAnimated: bool, PCur: int, PMax: int, MainDiv: bool = True) -> str:
+ """
+ Создание строки прогресса для HTML
+ :param Color: error, warning, blue, info, success
+ :param IsAnimated: True или False
+ :param PCur: Любое целое число
+ :param PMax: Любое целое число
+ :return: str
+ """
+ ProgressColor = ""
+
+ if (Color.lower() == "red" or Color.lower() == "danger" or Color.lower() == "error"):
+ ProgressColor = "bg-danger"
+
+ if (Color.lower() == "yellow" or Color.lower() == "warning"):
+ ProgressColor = "bg-warning"
+
+ if (Color.lower() == "green" or Color.lower() == "success"):
+ ProgressColor = "bg-success"
+
+ if (Color.lower() == "info"):
+ ProgressColor = "bg-info"
+
+ if (IsAnimated == True):
+ ProgressAnim = "progress-bar-animated"
+ else:
+ ProgressAnim = ""
+
+ if (PMax > 0):
+ PWidth = int(round(int(PCur) / int(PMax) * 100))
+ else:
+ PWidth = 100
+ PCur = 100
+
+ HTML = """"""
+ if (MainDiv == True):
+ HTML += """
"""
+
+ return HTML
+
+
+################################################################################
+def Now(Format: int = 0):
+ """
+ Текущая дата и время:
+ * 0 - в секундах (timestamp, unixtime, отчет с 01.01.1970): int
+ * 1 - ГГГГММДД: str
+ * 2 - ГГГГ-ММ-ДД: str
+ * 10 - в миллисекундах (без точки, для уникальности файла или временной таблицы) (timestamp, unixtime, отчет с 01.01.1970): str
+ * 102 - ГГГГ-ММ-ДД ЧЧ:ММ:СС: str
+ """
+
+ if (Format == 0):
+ return int(str(time.mktime(datetime.datetime.now().timetuple())).split('.')[0])
+ if (Format == 10):
+ return str(str(time.mktime(datetime.datetime.now().timetuple())).replace(".", ""))
+ if (Format == 1):
+ return str(datetime.datetime.now().strftime("%Y%m%d"))
+ if (Format == 2):
+ return str(datetime.datetime.now().strftime("%Y-%m-%d"))
+ if (Format == 102):
+ return str(datetime.datetime.now()).split('.')[0]
+ return ""
+
+
+################################################################################
+def SaveLog(Filename, Value, ShowTimeInLog=True):
+ """Запись лога в конец"""
+
+ CurTime = datetime.datetime.now()
+ Today = CurTime.strftime("%Y-%m-%d")
+
+ # Проверяем и создаем путь для логов
+ if (os.path.exists("logs") == False):
+ os.mkdir("logs")
+ if (os.path.exists("logs") == False):
+ SaveLog(NameModule, "Не удается создать папку logs!")
+ return
+
+ file = open("logs/" + Filename + "_" + str(Today) + ".log", "a")
+ if (ShowTimeInLog == True):
+ file.write(str(CurTime) + ": " + Value + "\r\n")
+ else:
+ file.write(Value + "\r\n")
+ file.close()
+
+ if (app.config['SHOWLOG'] == True or GetVariable('Debug') == "1"):
+ print(str(CurTime) + ": " + Value)
+
+
+###############################################################################
+def validate(date_text: str):
+ """
+ Проверка что значение является датой.
+ На выходе datetime.datetime
+ :param date_text: Проверяемая дата
+ :return datetime.datetime
+ """
+ try:
+ # Проверка что есть доли секунды
+ if (date_text.find(".") < 0):
+ # Нет
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S').strftime("%d.%m.%Y %H:%M:%S")
+ else:
+ # Есть
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S.%f').strftime("%d.%m.%Y %H:%M:%S")
+ except Exception: # as E:
+ if (date_text == "None"):
+ return ""
+ else:
+ return date_text #
+
+
+###############################################################################
+def SQLEscape(Query: str) -> str:
+ """
+ Экранирование одинарных кавычек для исключения SQL-инъеккции в переменных
+ :param Query: часть запроса для экранирования
+ """
+ return Query.replace("'", "''")
+
+
+###############################################################################
+def ThreadVars_Message() -> str:
+ """Полоска для сообщений на всех страницах"""
+
+ return """{{ThreadVars.message}}
"""
+
+
+###############################################################################
+def UserHead(Vars):
+ """
+ Чтение заголовка из файла block_head для шаблона
+ :param Vars
+ """
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_head.htm") == True):
+ file = open("app/templates/block_head.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+
+ CurStr = CurStr.replace("{{ProgName}}", Vars['ProgName'])
+ CurStr = CurStr.replace("{{Title}}", Vars['Title'])
+ return CurStr
+
+
+###############################################################################
+def UserHeader(SessionID: str, Vars: dict) -> str:
+ """
+ Чтение заголовка из файла block_header для шаблона
+ :param SessionID: ID-сессии
+ :param Vars: Массив переменных
+ """
+ CurDataSession: DataSession = CheckSession(SessionID, False)
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_header.htm") == True):
+ file = open("app/templates/block_header.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+
+ CurStr = CurStr.replace("{{IPServer}}", request.host.split(":")[0])
+ if (request.args.get("Name") is None):
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3])
+ else:
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3].replace("?Name=", "/"))
+ CurStr = CurStr.replace("{{ProgName}}", Vars['ProgName'])
+ CurStr = CurStr.replace("{{Title}}", Vars['Title'])
+ CurStr = CurStr.replace("{{FIO}}", CurDataSession.username)
+
+ Vars.update({"CurStr": CurStr})
+ Vars.update({"request": request})
+
+ return API_App.UserHeader(SessionID, Vars)
+
+
+###############################################################################
+def UserErrorHeader(SessionID: str, Vars: dict) -> str:
+ """
+ Чтение заголовка из файла block_error_header для шаблона
+ :param SessionID: ID-сессии
+ :param Vars: Массив переменных
+ """
+ CurDataSession: DataSession = CheckSession(SessionID, False)
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_error_header.htm") == True):
+ file = open("app/templates/block_error_header.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+
+ CurStr = CurStr.replace("{{IPServer}}", request.host.split(":")[0])
+ if (request.args.get("Name") is None):
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3])
+ else:
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3].replace("?Name=", "/"))
+ CurStr = CurStr.replace("{{ProgName}}", Vars['ProgName'])
+ CurStr = CurStr.replace("{{Title}}", Vars['Title'])
+ CurStr = CurStr.replace("{{FIO}}", CurDataSession.username)
+ return CurStr
+
+
+###############################################################################
+def Modals(Vars) -> str:
+ """Чтение из файла block_modals для шаблона
+ :param Vars: Массив переменных. Пока не используется.
+ """
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_modals.htm") == True):
+ file = open("app/templates/block_modals.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+ return CurStr
+
+
+###############################################################################
+def UpdateSession(SessionID: str):
+ """
+ Процедура продляет существование сессии
+ """
+
+ if (app.config['SHMNAME'] == 'kmsik'):
+ return API_App.UpdateSession(SessionID) # Нестандартное обновление сессии
+
+ # Если сессия не существует или уже просрочена, то ничего не делаем
+ CurDataSession: DataSession = CheckSession(SessionID, False)
+ if (CurDataSession.status == False):
+ return
+
+ if (CurDataSession.sessionid != SessionID):
+ return
+
+ # Продляем проверочную сессию
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ DEND = DBEG + int(app.config['MAX_TIME_SESSION']) # время сессии
+
+ # Продляем сессию еще на 15 минут
+ SaveDataSession(SessionID, "endsession", str(DEND))
+
+
+###############################################################################
+def ReadDataSession(SessionID, Variable, IsByte=False):
+ """
+ Считываем значение переменной сессии
+ :param SessionID: Сессия
+ :param Variable: Параметр
+ :param IsByte: Результат будет двоичные данные или нет
+ :return При нестандартной ситуации возвращает пустоту
+ """
+
+ TempVar = ""
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ SaveLog(NameModule, "ReadDataSession: Путь не существует: /dev/shm/" + app.config['SHMNAME'] + "")
+ return ""
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ SaveLog(NameModule, "ReadDataSession: Путь не существует: /dev/shm/" + app.config['SHMNAME'] + "/sessions")
+ return ""
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ if (os.path.exists(CurPath + "/" + Variable) == True):
+ if (IsByte == True):
+ file = open(CurPath + "/" + Variable, "rb")
+ else:
+ file = open(CurPath + "/" + Variable, "r")
+ TempVar = file.read()
+ file.close()
+ return TempVar
+ else:
+ SaveLog(NameModule, "ReadDataSession: Переменная не существует: " + CurPath + "/" + Variable)
+ else:
+ SaveLog(NameModule, "ReadDataSession: Путь не существует: " + CurPath)
+ return ""
+
+
+###############################################################################
+def SaveDataSession(SessionID, Variable, Value, IsByte=False) -> bool:
+ """
+ Сохраняем переменную сессии
+ :param SessionID: Сессия
+ :param Variable: Параметр
+ :param Value: Сохраняемое значение
+ :param IsByte: Сохранить бинарно или нет
+ :return При нестандартной ситуации возвращает False
+ """
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ return False
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ return False
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ if (IsByte == False):
+ file = open(CurPath + "/" + Variable, "w")
+ else:
+ file = open(CurPath + "/" + Variable, "wb")
+ file.write(Value)
+ file.close()
+ return True
+ return False
+
+
+###############################################################################
+def CheckSession(SessionID, FlagNeedUpdate) -> DataSession:
+ """
+ Процедура проверяет существование сессии
+ :param SessionID: Сессия
+ :param FlagNeedUpdate: Требуется ли обновление сессии при ее проверке
+ :return При существовании сессии .status = True
+ """
+ CurDataSession = DataSession()
+
+ # На пустую сессию ничего не возвращать
+ if (str(SessionID) == "" or SessionID is None):
+ CurDataSession.comment = "Не передан SessionID"
+ return CurDataSession
+
+ # Текущее время в секундах
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ CurTime = datetime.datetime.now()
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ CurDataSession.comment = "Не удается создать каталог для сессии"
+ return CurDataSession
+
+ # Проверяем наличие сессии
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ EndSession = ReadDataSession(SessionID, "endsession")
+
+ # Если нет информации об окончании сессии пробуем еще раз
+ if (EndSession == ""):
+ time.sleep(1)
+ EndSession = ReadDataSession(SessionID, "endsession")
+
+ # Если так нет информации об окончании сессии то пишем ошибку
+ if (EndSession == ""):
+ SaveLog(NameModule, "CheckSession: Нет информации об окончании сессии: " + SessionID + ".")
+ CurDataSession.status = False
+ CurDataSession.comment = "Нет информации об окончании сессии"
+ return CurDataSession
+
+ # Если сессия уже истекла, то удаляем её
+ if (float(DBEG) > float(EndSession)):
+ SaveLog(NameModule, str(CurTime) + ": Сессия " + SessionID + " истекла.")
+ DeleteSession(SessionID)
+ CurDataSession.comment = "Сессия истекла"
+ return CurDataSession
+
+ # Если всё ОК то заполняем данные
+ CurDataSession.userid = str(ReadDataSession(SessionID, "userid"))
+ CurDataSession.username = str(ReadDataSession(SessionID, "username"))
+ CurDataSession.stationid = str(ReadDataSession(SessionID, "stationid"))
+ CurDataSession.sessionid = SessionID
+
+ # Продлеваем если необходимо
+ if (FlagNeedUpdate == True):
+ UpdateSession(SessionID)
+
+ CurDataSession.status = True
+ CurDataSession.comment = "Сессия существует"
+ return CurDataSession # возвращаем данные сессии
+
+ CurDataSession.comment = "Сессия не существует"
+ return CurDataSession # возвращаем данные сессии
+
+
+###############################################################################
+def CreateSession(UserID: str, UserName: str, StationID: str = "", SessionID: str = "", IPClient: str = ""):
+ """Процедура создает сессию, после успешного определения авторизации
+ :param UserID
+ :param UserName
+ :param StationID: Не обязательный параметр
+ :param SessionID: Не обязательный параметр
+ :param IPClient: Не обязательный параметр
+ """
+ if (app.config['SHMNAME'] == 'kmsik'):
+ return API_App.CreateSession(UserID, UserName, StationID, SessionID, IPClient) # Для КМС-ИК
+
+ # Удаление старых сессии
+ DeleteExpiredSession()
+
+ # защита от создания сессии на пустой ID
+ if (UserID == "" or UserID == "0"):
+ return ""
+
+ # Переменные
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ DEND = DBEG + int(app.config['MAX_TIME_SESSION']) # время сессии
+ CurTime = datetime.datetime.now()
+ if (SessionID == ""):
+ SessionID = "S_" + str(uuid.uuid4())
+ SaveLog(NameModule, str(CurTime) + ": Создание сессии " + SessionID)
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # создаем файл для сессии
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ os.mkdir(CurPath)
+
+ # Сохраняем данные
+ SaveLog(NameModule, str(CurTime) + ": Сохраняем данные сессии " + SessionID)
+ SaveDataSession(SessionID, "ipclient", str(IPClient))
+ SaveDataSession(SessionID, "userid", str(UserID))
+ SaveDataSession(SessionID, "username", str(UserName))
+ SaveDataSession(SessionID, "startsession", str(DEND))
+ SaveDataSession(SessionID, "endsession", str(DEND))
+ SaveDataSession(SessionID, "stationid", str(StationID))
+
+ SaveLog(NameModule, "Успешный вход в систему: " + UserName)
+ return SessionID # возвращаем ID сессии
+
+
+###############################################################################
+def CreateSessionVK(UserID, UserName):
+ """
+ Процедура создает сессию, после успешного определения авторизации
+ :param UserID:
+ :param UserName:
+ """
+ # Удаление старых сессии
+ DeleteExpiredSession()
+
+ # защита от создания сессии на пустой ID
+ if (UserID == "" or UserID == "0"):
+ return ""
+
+ # Переменные
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ DEND = DBEG + int(app.config['MAX_TIME_SESSION']) # время сессии
+ SessionID = str(uuid.uuid4())
+ CurTime = datetime.datetime.now()
+ SaveLog(NameModule, str(CurTime) + ": Создание сессии ВК " + SessionID)
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # создаем файл для сессии
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ os.mkdir(CurPath)
+
+ # Сохраняем данные
+ SaveLog(NameModule, str(CurTime) + ": Сохраняем данные сессии " + SessionID)
+ SaveDataSession(SessionID, "userid", str(UserID))
+ SaveDataSession(SessionID, "username", str(UserName))
+ SaveDataSession(SessionID, "startsession", str(DEND))
+ SaveDataSession(SessionID, "endsession", str(DEND))
+ SaveDataSession(SessionID, "fromvk", '1')
+
+ SaveLog(NameModule, "Успешный вход в систему из под ВК: " + UserName)
+
+ return SessionID # возвращаем ID сессии
+
+
+###############################################################################
+def DeleteExpiredSession():
+ """Процедура удаляет из базы истекшие сессии"""
+
+ # Текущее время в секундах
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ CurTime = datetime.datetime.now()
+
+ SaveLog(NameModule, str(CurTime) + ": Запуск удаления старых сессии ")
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # Получаем каталог сессии
+ for SessionID in os.listdir(f"/dev/shm/{app.config['SHMNAME']}/sessions"):
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ # проверяем не файл ли это
+ if (os.path.isfile(CurPath) == False):
+ # проверяем срок сессии
+ EndSession = ReadDataSession(SessionID, "endsession")
+
+ if (EndSession == ""):
+ DeleteSession(SessionID)
+ else:
+ # Если сессия уже истекла, то удаляем её
+ if (float(DBEG) > float(EndSession)):
+ SaveLog(NameModule, str(CurTime) + ": Сессия " + SessionID + " истекла.")
+ DeleteSession(SessionID)
+
+
+###############################################################################
+def DeleteSession(SessionID):
+ """Процедура удаляет из базы конкретную сессию"""
+
+ CurTime = datetime.datetime.now()
+ if (SessionID == ""):
+ SaveLog(NameModule, str(CurTime) + ": Сессия для удаления не указана!")
+ return False
+
+ SaveLog(NameModule, str(CurTime) + ": Удаление сессии " + SessionID)
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # создаем файл для сессии
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID) == True):
+
+ # Удаляем все файлы сессии
+ for f in os.listdir(f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID):
+ os.remove(f"/dev/shm/{app.config['SHMNAME']}/sessions/{SessionID}/{f}")
+
+ # Удаляем папку сессии
+ os.rmdir(f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID)
+
+
+###############################################################################
+def GetVariable(Variable: str) -> str:
+ """
+ Считываем настройку программы
+ :param Variable: имя переменной
+ """
+
+ TempVar = ""
+
+ # Проверяем наличие сессии
+ CurPath = "db/Settings/"
+
+ # Если работает с Виртуальной памятью, то ищем настройки там
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/Settings") == True):
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/Settings/"
+
+ if (os.path.exists(CurPath) == True):
+ if (os.path.exists(CurPath + "/" + Variable) == True):
+ file = open(CurPath + "/" + Variable, "r")
+ TempVar = file.read()
+ file.close()
+ return TempVar.strip()
+
+ return ""
+
+
+###############################################################################
+def SaveVariable(Variable, Value):
+ """
+ Сохраняем переменную от настроек программы
+ :param Variable: Параметр
+ :param Value: Сохраняемое значение
+ """
+
+ # Проверяем пусть
+ if (os.path.exists("db") == False):
+ return ""
+ if (os.path.exists("db/Settings") == False):
+ return ""
+
+ # Сохраняем настройки в программе
+ CurPath = "db/Settings/"
+ if (os.path.exists(CurPath) == True):
+ file = open(CurPath + "/" + Variable, "w")
+ file.write(str(Value))
+ file.close()
+
+ # Сохраняем настройки в памяти
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/Settings/"
+ if (os.path.exists(CurPath) == True):
+ file = open(CurPath + "/" + Variable, "w")
+ file.write(str(Value))
+ file.close()
+
+ return True
+ return False
+
+
+###############################################################################
+def GetSHA512_FromFile(Filename: str, secret: str) -> str:
+ """
+ Получить хэш на основе секретного ключа
+ :param Filename: путь к файлу
+ :param secret:
+ """
+ if (os.path.exists(Filename) == False):
+ return ""
+
+ # Расчет хэша
+ file = open(Filename, "rb")
+ CurStr = file.read()
+ file.close()
+ return GetSHA512_File(CurStr, secret)
+
+
+###############################################################################
+def GetSHA512_File(message: bytes, secret: str) -> str:
+ """
+ Получить хэш из массива байтов на основе секретного ключа
+ :param message: bytes
+ :param secret
+ """
+
+ h = hmac.new(bytearray(secret, "UTF-8"), message, hashlib.sha512)
+ return base64.b64encode(h.digest()).decode()
+
+
+###############################################################################
+def GetSHA512_Text(message: str, secret: str) -> str:
+ """Получить хэш из текста на основе секретного ключа"""
+ h = hmac.new(bytearray(secret, "UTF-8"), bytearray(message, "UTF-8"), hashlib.sha512)
+ return base64.b64encode(h.digest()).decode()
+
+
+###############################################################################
+def GetSHA1_Text(message: str) -> str:
+ """Получить хэш из текста на основе секретного ключа. Совместим с хэшем SQL 2005"""
+ hash_object = hashlib.sha1(bytearray(message, "UTF-8"))
+ hex_dig = hash_object.hexdigest()
+ return hex_dig
+
+
+###############################################################################
+def CheckPort(ip, port) -> str:
+ """
+ Проверка порта на его открытие
+ "OPEN" или "CLOSE"
+ """
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1)
+
+ ip = ip.split("\\")[0]
+ Attempt = 0
+ LastStatus = ""
+ SaveLog(NameModule, "Проверка порта на: " + ip + ":" + str(port))
+
+ # Проверяем трижды или до 1 успеха
+ while (Attempt < 3):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1)
+ try:
+ sock.connect((ip, int(port)))
+ SaveLog(NameModule, 'CheckPort: Порт ' + str(port) + ' открыт.')
+ # conn.close()
+ return "OPEN"
+ except Exception as E:
+ SaveLog(NameModule, "CheckPort: " + str(E))
+ # LastStatus = "ERROR"
+
+ time.sleep(1)
+ LastStatus = "CLOSE"
+ Attempt = Attempt + 1
+ SaveLog(NameModule, 'CheckPort: Порт ' + str(port) + ' закрыт.')
+ return LastStatus
+
+
+###############################################################################
+def RenameDataSession(SessionID, OldNameVar, NewNameVar) -> bool:
+ """
+ Переименовываем переменную сессии
+ """
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ return False
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ return False
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ os.rename(CurPath + "/" + OldNameVar, CurPath + "/" + NewNameVar)
+ return True
+ return False
+
+
+###############################################################################
+def RemoveFromFolder(folder, RemoveSelf=False) -> str:
+ """
+ Удалить всё из указанной папки
+ * RemoveSelf - Удалить саму папку (уже пустую)
+ return: Текст ошибки
+ """
+
+ print('Обработка папки на удаление : ' + folder)
+ for filename in os.listdir(folder): # Получаем список файлов в папке
+ file_path = os.path.join(folder, filename) # Полный путь к файлу или папке
+ try:
+ if os.path.isfile(file_path): # Если это файл
+ print('Удаляем файл: ' + file_path)
+ os.remove(file_path) # Удалить
+ elif os.path.isdir(file_path): # Если это папка
+ RemoveFromFolder(file_path) # Удаляем содержимое этой папки (рекурсия)
+ print('Удаляем папку : ' + file_path)
+ os.rmdir(file_path) # А затем удалить саму эту папку
+ except Exception as e:
+ TextLog = f'Не удалось удалить {file_path}. Причина: {e}'
+ SaveLog(NameModule, TextLog)
+ return TextLog
+ if (RemoveSelf == True):
+ print('Удаляем папку : ' + folder)
+ os.rmdir(folder) # А затем удалить саму эту папку
+ print('Обработка папки на удаление завершена: ' + folder)
+ return ""
+
+
+###############################################################################
+def RemoveDataSession(SessionID, Variable):
+ """
+ Удаляем переменную сессии
+ """
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ return False
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ return False
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ if (os.path.isfile(CurPath + "/" + Variable)):
+ os.remove(CurPath + "/" + Variable)
+ return True
+ return False
+
+
+########################################################################
+def GetSHA1_FromFile(Filename):
+ """Хэш для совместимости с MSSQL"""
+ if (os.path.exists(Filename) == False):
+ return ""
+
+ # Расчет хэша
+ file = open(Filename, "rb")
+ CurStr = file.read()
+ file.close()
+
+ hash_object = hashlib.sha1(CurStr)
+ hex_dig = hash_object.hexdigest()
+
+ return hex_dig
+
+
+########################################################################
+def ConvertStrToDate(date_text: str) -> datetime.datetime:
+ """
+ Проверка что строка является датой.
+ * Поддерживается ГГГГММДД, ГГГГ-ММ-ДД, ДД.ММ.ГГГГ.
+ * Поддерживается ГГГГ-ММ-ДД ЧЧ:ММ
+ * Поддерживается ГГГГ-ММ-ДД ЧЧ:ММ:СС
+ * Поддерживается ГГГГ-ММ-ДД ЧЧ:ММ:СС.мс
+ * Поддерживается с буквой T вместо пробела.
+
+
+ """
+ try:
+
+ date_text = date_text.strip()
+
+ # Если слишком мало текста для преобразования
+ # Дата не указана полностью
+ if (len(date_text) < 8 or len(date_text) == 9):
+ return None # type: ignore
+
+ # нет секунд. Вставляем их.
+ # print("date_text: " + date_text)
+ # print(f"len date_text: {len(date_text)}")
+ # print(f"""date_text.find("T": {date_text.find("T")}""")
+
+ if (len(date_text) == 16 and (date_text.find("T") == 10)):
+ date_text = date_text + ":00"
+ print("date_text: " + date_text)
+
+ # время не указано полностью
+ if (len(date_text) > 10 and len(date_text) < 19):
+ return None # type: ignore
+
+ # не 1-х и не 2-х тысячалетие
+ if (len(date_text) == 8 and date_text[:1] not in ('1', '2')):
+ return None # type: ignore
+
+ # ГГГГММДД
+ if (len(date_text) == 8):
+ return datetime.datetime.strptime(date_text, '%Y%m%d')
+
+ # Если точка идет 3-им символов (для формата ДД.ММ.ГГГГ )
+ if (date_text.find('.') == 2 or date_text.find('/') == 2 or date_text.find('-') == 2):
+ date_text = f"{date_text[6:10]}-{date_text[3:5]}-{date_text[0:2]}"
+
+ if (len(date_text) == 10):
+ return datetime.datetime.strptime(date_text.replace(".", "-"), '%Y-%m-%d')
+
+ # Если время разделено буквой T
+ if (date_text.find("T") > 0):
+ if (date_text.find(".") < 0): # Проверка что есть доли секунды
+ # Нет
+ return datetime.datetime.strptime(date_text, '%Y-%m-%dT%H:%M:%S')
+ else:
+ # Есть
+ return datetime.datetime.strptime(date_text, '%Y-%m-%dT%H:%M:%S.%f')
+ else:
+ # Проверка что есть доли секунды
+ if (date_text.find(".") < 0):
+ # Нет
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S')
+ else:
+ # Есть
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S.%f')
+ except Exception:
+ if (date_text == "None"):
+ return None # type: ignore
+ else:
+ return date_text # type: ignore
+
+
+###############################################################################
+def ConvertDate(d: datetime.datetime, NewType=102) -> str:
+ """
+ Преобразование даты в указанный формат:
+ * 102 - ГГГГ-ММ-ДД
+ * 104 - ДД.ММ.ГГГГ
+ * 107 - январь 2023
+ * 108 - Январь 2023
+ * 109 - ЯНВАРЬ 2023
+ * 112 - ГГГГММДД
+ * 122 - ГГГГ-ММ-ДД ЧЧ:ММ:СС
+
+ """
+ M = {}
+ M.update({"01": "январь"})
+ M.update({"02": "февраль"})
+ M.update({"03": "март"})
+ M.update({"04": "апрель"})
+ M.update({"05": "май"})
+ M.update({"06": "июнь"})
+ M.update({"07": "июль"})
+ M.update({"08": "август"})
+ M.update({"09": "сентябрь"})
+ M.update({"10": "октябрь"})
+ M.update({"11": "ноябрь"})
+ M.update({"12": "декабрь"})
+
+ if NewType == 102:
+ return d.strftime("%Y-%m-%d")
+ if NewType == 104:
+ return d.strftime("%d.%m.%Y")
+
+ if NewType == 107:
+ return M[d.strftime("%m")] + " " + d.strftime("%Y")
+ if NewType == 108:
+ return M[d.strftime("%m")].capitalize() + " " + d.strftime("%Y")
+ if NewType == 109:
+ return M[d.strftime("%m")].upper() + " " + d.strftime("%Y")
+
+ if NewType == 112:
+ return d.strftime("%Y%m%d")
+
+ if NewType == 122:
+ return d.strftime('%Y-%m-%d %H:%M:%S')
+
+ return ""
+
+
+###############################################################################
+def GetTZ():
+ """Получить текущий часовой пояс"""
+
+ now = datetime.datetime.now()
+ Z = str(now.replace(tzinfo=datetime.timezone.utc) - now.astimezone(datetime.timezone.utc)).split(':')[0]
+ return int(Z)
+
+
+###############################################################################
+def GetStatusService(ServiceName) -> str:
+ # Проверка статуса
+ Result = subprocess.run("systemctl -all | grep " + ServiceName, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+
+ if (str(Result.returncode) == "1"):
+ return "None"
+
+ # Проверка статуса
+ p = subprocess.Popen(["systemctl", "is-active", ServiceName], stdout=subprocess.PIPE)
+ (output, err) = p.communicate()
+ output = output.decode('utf-8')
+ # print(output)
+ return output.strip()
+
+
+###############################################################################
+def CreateDBSettings():
+ """Создание базы и таблицы настроек"""
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("db") == False):
+ os.mkdir("db")
+ if (os.path.exists("db/Settings") == False):
+ os.mkdir("db/Settings")
+
+
+###############################################################################
+def GetFromRequestsT(Url: str, Attempt=1, headers={}) -> str:
+ """Получить ответ по HTTP/S ссылке"""
+
+ if (headers == {}):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+ # TODO: Узнать бы какой сертификат тут используется.
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0 or Url.find('rosminzdrav.ru') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем GET-запрос на URL
+ response = requests.get(Url, headers=headers, timeout=600, verify=CheckCert)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return GetFromRequestsT(Url, Attempt + 1)
+ else:
+ return ""
+
+ # Возвращаем тело ответа в виде строки
+ return response.text
+
+
+###############################################################################
+def GetFromRequestsB(Url: str, Attempt=1, headers={}) -> bytes:
+ """
+ Получить ответ по HTTP/S ссылке.
+ Ответ в байтах. Например, скачаный двоичный файл.
+ """
+
+ if (headers == {}):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+ # TODO: Узнать бы какой сертификат тут используется.
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0 or Url.find('rosminzdrav.ru') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем GET-запрос на URL
+ response = requests.get(Url, headers=headers, timeout=600, verify=CheckCert)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return GetFromRequestsB(Url, Attempt + 1)
+ else:
+ return None # type: ignore
+
+ # Или в виде байтов
+ return response.content
+
+
+###############################################################################
+def PostFromRequestsB(Url: str, Data, Attempt=1, headers=None) -> bytes:
+ """
+ Получить ответ по HTTP/S ссылке.
+ Ответ в байтах. Например, скачаный двоичный файл.
+ """
+
+ if (headers is None):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+ # TODO: Узнать бы какой сертификат тут используется.
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0 or Url.find('rosminzdrav.ru') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем POST-запрос на URL
+ response = requests.post(Url, headers=headers, timeout=600, verify=CheckCert, data=Data)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return PostFromRequestsB(Url, Data, Attempt + 1)
+ else:
+ return None
+
+ # Или в виде байтов
+ return response.content
+
+
+###############################################################################
+def PostFromRequestsT(Url: str, data, Attempt=1, headers=None) -> dict:
+ """
+ Получить ответ по HTTP/S ссылке.
+ Ответ в виде текста.
+ """
+ Answer = {}
+ if (headers is None):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+
+ # TODO: Узнать бы какой сертификат тут используется.
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0 or Url.find('rosminzdrav.ru') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем POST-запрос на URL
+ response = requests.post(Url, headers=headers, timeout=600, verify=CheckCert, data=data)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+ Answer.update({"code": str(status_code)})
+ Answer.update({"text": response.text})
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return PostFromRequestsT(Url, data, Attempt + 1)
+ else:
+ Answer.update({"status": "error"})
+ return Answer
+
+ if (str(status_code)[0] == "2"):
+ Answer.update({"status": "ok"})
+ else:
+ Answer.update({"status": "error"})
+
+ # Или в виде текста
+ return Answer
+
+
+###############################################################################
+def DayWeek(CurDate: datetime.datetime = datetime.datetime.today()):
+ "Получить день недели (по умолчанию - сегодня)"
+ weekday = CurDate.weekday()
+ return weekday + 1 # Что бы было от 1 до 7
+
+
+###############################################################################
+def SaveFile(Filename, Content, IsBinary=False) -> str:
+ """
+ Сохранить файл с заменой существующего файла.
+ * Filename - полный путь к файлу.
+ * Content - содержимое файла.
+ * IsBinary - является ли содержимое двоичным или нет.
+ return - пустота если всё хорошо или текст ошибки.
+ """
+ Mode = 'w' # Текстовый
+ if (IsBinary == True):
+ Mode = 'wb' # Бинарный
+
+ try:
+ with open(Filename, Mode) as fp:
+ fp.write(Content)
+ fp.close()
+ return '' # Всё хорошо
+ except Exception as E:
+ return str(E)
+
+
+###############################################################################
+def NewGUID() -> str:
+ "Сгененрировать новый GUID"
+ return str(uuid.uuid4())
diff --git a/app/source/OLD_API_Common.py b/app/source/OLD_API_Common.py
new file mode 100644
index 0000000..6a76950
--- /dev/null
+++ b/app/source/OLD_API_Common.py
@@ -0,0 +1,1084 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+API с общими функциями и процедурами
+Дата последней оптимизации: 22.05.2022
+Дата обновления: 11.01.2023
+"""
+
+import subprocess
+import uuid # Генератор GUID
+import datetime # Работа с датой
+import time # работа с временем
+import os # Работа с файловой системой
+import requests # Работа с http/s
+
+# Для Криптографии
+import hmac
+import base64
+import hashlib
+
+import socket # Для проверки доступности портов
+from app import app # чтение из config.py
+from flask import request # Для получения метаданных о запросе
+
+import sys # Подключаем свои библиотеки
+sys.path.append("app/source")
+import API_App
+
+appname = app.config['SHMNAME']
+NameModule = "API_Common"
+
+
+################################################################################
+class DataSession:
+ """Класс для передачи данных о сесссии"""
+
+ def __init__(self):
+ self.status: bool = False # Действует сессия или нет
+ self.comment: str = ""
+ self.sessionid: str = ""
+ self.userid: str = ""
+ self.username: str = ""
+ self.stationid: str = ""
+
+
+################################################################################
+def HTML_Progress(Color, IsAnimated: bool, PCur: int, PMax: int,) -> str:
+ """
+ Создание строки прогресса для HTML
+ :param Color: error, warning, blue, info, success
+ :param IsAnimated: True или False
+ :param PCur: Любое целое число
+ :param PMax: Любое целое число
+ :return: str
+ """
+ ProgressColor = ""
+
+ if (Color.lower() == "red" or Color.lower() == "danger" or Color.lower() == "error"):
+ ProgressColor = "bg-danger"
+
+ if (Color.lower() == "yellow" or Color.lower() == "warning"):
+ ProgressColor = "bg-warning"
+
+ if (Color.lower() == "green" or Color.lower() == "success"):
+ ProgressColor = "bg-success"
+
+ if (Color.lower() == "info"):
+ ProgressColor = "bg-info"
+
+ if (IsAnimated == True):
+ ProgressAnim = "progress-bar-animated"
+ else:
+ ProgressAnim = ""
+
+ if (PMax > 0):
+ PWidth = int(round(int(PCur) / int(PMax) * 100))
+ else:
+ PWidth = 100
+ PCur = 100
+
+ return f""""""
+
+
+################################################################################
+def Now(Format: int = 0):
+ """
+ Текущая дата и время:
+ * 0 - в секундах (timestamp, unixtime, отчет с 01.01.1970): int
+ * 1 - ГГГГММДД: str
+ * 2 - ГГГГ-ММ-ДД: str
+ * 10 - в миллисекундах (без точки, для уникальности файла или временной таблицы) (timestamp, unixtime, отчет с 01.01.1970): str
+ * 102 - ГГГГ-ММ-ДД ЧЧ:ММ:СС: str
+ """
+
+ if (Format == 0):
+ return int(str(time.mktime(datetime.datetime.now().timetuple())).split('.')[0])
+ if (Format == 10):
+ return str(str(time.mktime(datetime.datetime.now().timetuple())).replace(".", ""))
+ if (Format == 1):
+ return str(datetime.datetime.now().strftime("%Y%m%d"))
+ if (Format == 2):
+ return str(datetime.datetime.now().strftime("%Y-%m-%d"))
+ if (Format == 102):
+ return str(datetime.datetime.now()).split('.')[0]
+ return ""
+
+################################################################################
+
+
+def SaveLog(Filename, Value, ShowTimeInLog=True):
+ """Запись лога в конец"""
+
+ CurTime = datetime.datetime.now()
+ Today = CurTime.strftime("%Y-%m-%d")
+
+ # Проверяем и создаем путь для логов
+ if (os.path.exists("logs") == False):
+ os.mkdir("logs")
+ if (os.path.exists("logs") == False):
+ SaveLog(NameModule, "Не удается создать папку logs!")
+ return
+
+ file = open("logs/" + Filename + "_" + str(Today) + ".log", "a")
+ if (ShowTimeInLog == True):
+ file.write(str(CurTime) + ": " + Value + "\r\n")
+ else:
+ file.write(Value + "\r\n")
+ file.close()
+
+ if (app.config['SHOWLOG'] == True or GetVariable('Debug') == "1"):
+ print(str(CurTime) + ": " + Value)
+
+
+###############################################################################
+def validate(date_text: str):
+ """
+ Проверка что значение является датой.
+ На выходе datetime.datetime
+ :param date_text: Проверяемая дата
+ :return datetime.datetime
+ """
+ try:
+ # Проверка что есть доли секунды
+ if (date_text.find(".") < 0):
+ # Нет
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S').strftime("%d.%m.%Y %H:%M:%S")
+ else:
+ # Есть
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S.%f').strftime("%d.%m.%Y %H:%M:%S")
+ except Exception: # as E:
+ if (date_text == "None"):
+ return ""
+ else:
+ return date_text #
+
+
+###############################################################################
+def SQLEscape(Query: str) -> str:
+ """
+ Экранирование одинарных кавычек для исключения SQL-инъеккции в переменных
+ :param Query: часть запроса для экранирования
+ """
+ return Query.replace("'", "''")
+
+
+###############################################################################
+def ThreadVars_Message() -> str:
+ """Полоска для сообщений на всех страницах"""
+
+ return """{{ThreadVars.message}}
"""
+
+
+###############################################################################
+def UserHead(Vars):
+ """
+ Чтение заголовка из файла block_head для шаблона
+ :param Vars
+ """
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_head.htm") == True):
+ file = open("app/templates/block_head.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+
+ CurStr = CurStr.replace("{{ProgName}}", Vars['ProgName'])
+ CurStr = CurStr.replace("{{Title}}", Vars['Title'])
+ return CurStr
+
+
+###############################################################################
+def UserHeader(SessionID: str, Vars: dict) -> str:
+ """
+ Чтение заголовка из файла block_header для шаблона
+ :param SessionID: ID-сессии
+ :param Vars: Массив переменных
+ """
+ CurDataSession: DataSession = CheckSession(SessionID, False)
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_header.htm") == True):
+ file = open("app/templates/block_header.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+
+ CurStr = CurStr.replace("{{IPServer}}", request.host.split(":")[0])
+ if (request.args.get("Name") is None):
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3])
+ else:
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3].replace("?Name=", "/"))
+ CurStr = CurStr.replace("{{ProgName}}", Vars['ProgName'])
+ CurStr = CurStr.replace("{{Title}}", Vars['Title'])
+ CurStr = CurStr.replace("{{FIO}}", CurDataSession.username)
+
+ Vars.update({"CurStr": CurStr})
+ Vars.update({"request": request})
+
+ return API_App.UserHeader(SessionID, Vars)
+
+
+###############################################################################
+def UserErrorHeader(SessionID: str, Vars: dict) -> str:
+ """
+ Чтение заголовка из файла block_error_header для шаблона
+ :param SessionID: ID-сессии
+ :param Vars: Массив переменных
+ """
+ CurDataSession: DataSession = CheckSession(SessionID, False)
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_error_header.htm") == True):
+ file = open("app/templates/block_error_header.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+
+ CurStr = CurStr.replace("{{IPServer}}", request.host.split(":")[0])
+ if (request.args.get("Name") is None):
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3])
+ else:
+ CurStr = CurStr.replace("{{PageName}}", request.url.split('/')[3].replace("?Name=", "/"))
+ CurStr = CurStr.replace("{{ProgName}}", Vars['ProgName'])
+ CurStr = CurStr.replace("{{Title}}", Vars['Title'])
+ CurStr = CurStr.replace("{{FIO}}", CurDataSession.username)
+ return CurStr
+
+
+###############################################################################
+def Modals(Vars) -> str:
+ """Чтение из файла block_modals для шаблона
+ :param Vars: Массив переменных. Пока не используется.
+ """
+ CurStr = ""
+
+ if (os.path.exists("app/templates/block_modals.htm") == True):
+ file = open("app/templates/block_modals.htm", "r")
+ # SaveLog(NameModule, "Чтение заголовка")
+ CurStr = file.read()
+ file.close()
+ return CurStr
+
+
+###############################################################################
+def UpdateSession(SessionID: str):
+ """
+ Процедура продляет существование сессии
+ """
+
+ if (app.config['SHMNAME'] == 'kmsik'): return API_App.UpdateSession(SessionID) # Нестандартное обновление сессии
+
+ # Если сессия не существует или уже просрочена, то ничего не делаем
+ CurDataSession: DataSession = CheckSession(SessionID, False)
+ if (CurDataSession.status == False): return
+
+ if (CurDataSession.sessionid != SessionID): return
+
+ # Продляем проверочную сессию
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ DEND = DBEG + int(app.config['MAX_TIME_SESSION']) # время сессии
+
+ # Продляем сессию еще на 15 минут
+ SaveDataSession(SessionID, "endsession", str(DEND))
+
+
+###############################################################################
+def ReadDataSession(SessionID, Variable, IsByte=False):
+ """
+ Считываем значение переменной сессии
+ :param SessionID: Сессия
+ :param Variable: Параметр
+ :param IsByte: Результат будет двоичные данные или нет
+ :return При нестандартной ситуации возвращает пустоту
+ """
+
+ TempVar = ""
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ SaveLog(NameModule, "ReadDataSession: Путь не существует: /dev/shm/" + app.config['SHMNAME'] + "")
+ return ""
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ SaveLog(NameModule, "ReadDataSession: Путь не существует: /dev/shm/" + app.config['SHMNAME'] + "/sessions")
+ return ""
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ if (os.path.exists(CurPath + "/" + Variable) == True):
+ if (IsByte == True):
+ file = open(CurPath + "/" + Variable, "rb")
+ else:
+ file = open(CurPath + "/" + Variable, "r")
+ TempVar = file.read()
+ file.close()
+ return TempVar
+ else:
+ SaveLog(NameModule, "ReadDataSession: Переменная не существует: " + CurPath + "/" + Variable)
+ else:
+ SaveLog(NameModule, "ReadDataSession: Путь не существует: " + CurPath)
+ return ""
+
+
+###############################################################################
+def SaveDataSession(SessionID, Variable, Value, IsByte=False) -> bool:
+ """
+ Сохраняем переменную сессии
+ :param SessionID: Сессия
+ :param Variable: Параметр
+ :param Value: Сохраняемое значение
+ :param IsByte: Сохранить бинарно или нет
+ :return При нестандартной ситуации возвращает False
+ """
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ return False
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ return False
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ if (IsByte == False):
+ file = open(CurPath + "/" + Variable, "w")
+ else:
+ file = open(CurPath + "/" + Variable, "wb")
+ file.write(Value)
+ file.close()
+ return True
+ return False
+
+
+###############################################################################
+def CheckSession(SessionID, FlagNeedUpdate) -> DataSession:
+ """
+ Процедура проверяет существование сессии
+ :param SessionID: Сессия
+ :param FlagNeedUpdate: Требуется ли обновление сессии при ее проверке
+ :return При существовании сессии .status = True
+ """
+ CurDataSession = DataSession()
+
+ # На пустую сессию ничего не возвращать
+ if (str(SessionID) == "" or SessionID is None):
+ CurDataSession.comment = "Не передан SessionID"
+ return CurDataSession
+
+ # Текущее время в секундах
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ CurTime = datetime.datetime.now()
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ CurDataSession.comment = "Не удается создать каталог для сессии"
+ return CurDataSession
+
+ # Проверяем наличие сессии
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ EndSession = ReadDataSession(SessionID, "endsession")
+
+ # Если нет информации об окончании сессии пробуем еще раз
+ if (EndSession == ""):
+ time.sleep(1)
+ EndSession = ReadDataSession(SessionID, "endsession")
+
+ # Если так нет информации об окончании сессии то пишем ошибку
+ if (EndSession == ""):
+ SaveLog(NameModule, "CheckSession: Нет информации об окончании сессии: " + SessionID + ".")
+ CurDataSession.status = False
+ CurDataSession.comment = "Нет информации об окончании сессии"
+ return CurDataSession
+
+ # Если сессия уже истекла, то удаляем её
+ if (float(DBEG) > float(EndSession)):
+ SaveLog(NameModule, str(CurTime) + ": Сессия " + SessionID + " истекла.")
+ DeleteSession(SessionID)
+ CurDataSession.comment = "Сессия истекла"
+ return CurDataSession
+
+ # Если всё ОК то заполняем данные
+ CurDataSession.userid = str(ReadDataSession(SessionID, "userid"))
+ CurDataSession.username = str(ReadDataSession(SessionID, "username"))
+ CurDataSession.stationid = str(ReadDataSession(SessionID, "stationid"))
+ CurDataSession.sessionid = SessionID
+
+ # Продлеваем если необходимо
+ if (FlagNeedUpdate == True): UpdateSession(SessionID)
+
+ CurDataSession.status = True
+ CurDataSession.comment = "Сессия существует"
+ return CurDataSession # возвращаем данные сессии
+
+ CurDataSession.comment = "Сессия не существует"
+ return CurDataSession # возвращаем данные сессии
+
+
+###############################################################################
+def CreateSession(UserID: str, UserName: str, StationID: str = "", SessionID: str = "", IPClient: str = ""):
+ """Процедура создает сессию, после успешного определения авторизации
+ :param UserID
+ :param UserName
+ :param StationID: Не обязательный параметр
+ :param SessionID: Не обязательный параметр
+ :param IPClient: Не обязательный параметр
+ """
+ if (app.config['SHMNAME'] == 'kmsik'): return API_App.CreateSession(UserID, UserName, StationID, SessionID, IPClient) # Для КМС-ИК
+
+ # Удаление старых сессии
+ DeleteExpiredSession()
+
+ # защита от создания сессии на пустой ID
+ if (UserID == "" or UserID == "0"):
+ return ""
+
+ # Переменные
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ DEND = DBEG + int(app.config['MAX_TIME_SESSION']) # время сессии
+ CurTime = datetime.datetime.now()
+ if (SessionID == ""): SessionID = "S_" + str(uuid.uuid4())
+ SaveLog(NameModule, str(CurTime) + ": Создание сессии " + SessionID)
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # создаем файл для сессии
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ os.mkdir(CurPath)
+
+ # Сохраняем данные
+ SaveLog(NameModule, str(CurTime) + ": Сохраняем данные сессии " + SessionID)
+ SaveDataSession(SessionID, "ipclient", str(IPClient))
+ SaveDataSession(SessionID, "userid", str(UserID))
+ SaveDataSession(SessionID, "username", str(UserName))
+ SaveDataSession(SessionID, "startsession", str(DEND))
+ SaveDataSession(SessionID, "endsession", str(DEND))
+ SaveDataSession(SessionID, "stationid", str(StationID))
+
+ SaveLog(NameModule, "Успешный вход в систему: " + UserName)
+ return SessionID # возвращаем ID сессии
+
+
+###############################################################################
+def CreateSessionVK(UserID, UserName):
+ """
+ Процедура создает сессию, после успешного определения авторизации
+ :param UserID:
+ :param UserName:
+ """
+ # Удаление старых сессии
+ DeleteExpiredSession()
+
+ # защита от создания сессии на пустой ID
+ if (UserID == "" or UserID == "0"):
+ return ""
+
+ # Переменные
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ DEND = DBEG + int(app.config['MAX_TIME_SESSION']) # время сессии
+ SessionID = str(uuid.uuid4())
+ CurTime = datetime.datetime.now()
+ SaveLog(NameModule, str(CurTime) + ": Создание сессии ВК " + SessionID)
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # создаем файл для сессии
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ os.mkdir(CurPath)
+
+ # Сохраняем данные
+ SaveLog(NameModule, str(CurTime) + ": Сохраняем данные сессии " + SessionID)
+ SaveDataSession(SessionID, "userid", str(UserID))
+ SaveDataSession(SessionID, "username", str(UserName))
+ SaveDataSession(SessionID, "startsession", str(DEND))
+ SaveDataSession(SessionID, "endsession", str(DEND))
+ SaveDataSession(SessionID, "fromvk", '1')
+
+ SaveLog(NameModule, "Успешный вход в систему из под ВК: " + UserName)
+
+ return SessionID # возвращаем ID сессии
+
+
+###############################################################################
+def DeleteExpiredSession():
+ """Процедура удаляет из базы истекшие сессии"""
+
+ # Текущее время в секундах
+ DBEG = time.mktime(datetime.datetime.now().timetuple())
+ CurTime = datetime.datetime.now()
+
+ SaveLog(NameModule, str(CurTime) + ": Запуск удаления старых сессии ")
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # Получаем каталог сессии
+ for SessionID in os.listdir(f"/dev/shm/{app.config['SHMNAME']}/sessions"):
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID
+ # проверяем не файл ли это
+ if (os.path.isfile(CurPath) == False):
+ # проверяем срок сессии
+ EndSession = ReadDataSession(SessionID, "endsession")
+
+ if (EndSession == ""):
+ DeleteSession(SessionID)
+ else:
+ # Если сессия уже истекла, то удаляем её
+ if (float(DBEG) > float(EndSession)):
+ SaveLog(NameModule, str(CurTime) + ": Сессия " + SessionID + " истекла.")
+ DeleteSession(SessionID)
+
+
+###############################################################################
+def DeleteSession(SessionID):
+ """Процедура удаляет из базы конкретную сессию"""
+
+ CurTime = datetime.datetime.now()
+ if (SessionID == ""):
+ SaveLog(NameModule, str(CurTime) + ": Сессия для удаления не указана!")
+ return False
+
+ SaveLog(NameModule, str(CurTime) + ": Удаление сессии " + SessionID)
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}")
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions") == False):
+ os.mkdir(f"/dev/shm/{app.config['SHMNAME']}/sessions")
+
+ # создаем файл для сессии
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID) == True):
+
+ # Удаляем все файлы сессии
+ for f in os.listdir(f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID):
+ os.remove(f"/dev/shm/{app.config['SHMNAME']}/sessions/{SessionID}/{f}")
+
+ # Удаляем папку сессии
+ os.rmdir(f"/dev/shm/{app.config['SHMNAME']}/sessions/" + SessionID)
+
+
+###############################################################################
+def GetVariable(Variable: str) -> str:
+ """
+ Считываем настройку программы
+ :param Variable: имя переменной
+ """
+
+ TempVar = ""
+
+ # Проверяем наличие сессии
+ CurPath = "db/Settings/"
+
+ # Если работает с Виртуальной памятью, то ищем настройки там
+ if (os.path.exists(f"/dev/shm/{app.config['SHMNAME']}/Settings") == True):
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/Settings/"
+
+ if (os.path.exists(CurPath) == True):
+ if (os.path.exists(CurPath + "/" + Variable) == True):
+ file = open(CurPath + "/" + Variable, "r")
+ TempVar = file.read()
+ file.close()
+ return TempVar.strip()
+
+ return ""
+
+
+###############################################################################
+def SaveVariable(Variable, Value):
+ """
+ Сохраняем переменную от настроек программы
+ :param Variable: Параметр
+ :param Value: Сохраняемое значение
+ """
+
+ # Проверяем пусть
+ if (os.path.exists("db") == False):
+ return ""
+ if (os.path.exists("db/Settings") == False):
+ return ""
+
+ # Сохраняем настройки в программе
+ CurPath = "db/Settings/"
+ if (os.path.exists(CurPath) == True):
+ file = open(CurPath + "/" + Variable, "w")
+ file.write(str(Value))
+ file.close()
+
+ # Сохраняем настройки в памяти
+ CurPath = f"/dev/shm/{app.config['SHMNAME']}/Settings/"
+ if (os.path.exists(CurPath) == True):
+ file = open(CurPath + "/" + Variable, "w")
+ file.write(str(Value))
+ file.close()
+
+ return True
+ return False
+
+
+###############################################################################
+def GetSHA512_FromFile(Filename: str, secret: str) -> str:
+ """
+ Получить хэш на основе секретного ключа
+ :param Filename: путь к файлу
+ :param secret:
+ """
+ if (os.path.exists(Filename) == False): return ""
+
+ # Расчет хэша
+ file = open(Filename, "rb")
+ CurStr = file.read()
+ file.close()
+ return GetSHA512_File(CurStr, secret)
+
+
+###############################################################################
+def GetSHA512_File(message: bytes, secret: str) -> str:
+ """
+ Получить хэш из массива байтов на основе секретного ключа
+ :param message: bytes
+ :param secret
+ """
+
+ h = hmac.new(bytearray(secret, "UTF-8"), message, hashlib.sha512)
+ return base64.b64encode(h.digest()).decode()
+
+
+###############################################################################
+def GetSHA512_Text(message: str, secret: str) -> str:
+ """Получить хэш из текста на основе секретного ключа"""
+ h = hmac.new(bytearray(secret, "UTF-8"), bytearray(message, "UTF-8"), hashlib.sha512)
+ return base64.b64encode(h.digest()).decode()
+
+
+###############################################################################
+def GetSHA1_Text(message: str) -> str:
+ """Получить хэш из текста на основе секретного ключа. Совместим с хэшем SQL 2005"""
+ hash_object = hashlib.sha1(bytearray(message, "UTF-8"))
+ hex_dig = hash_object.hexdigest()
+ return hex_dig
+
+
+###############################################################################
+def CheckPort(ip, port) -> str:
+ """
+ Проверка порта на его открытие
+ "OPEN" или "CLOSE"
+ """
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1)
+
+ ip = ip.split("\\")[0]
+ Attempt = 0
+ LastStatus = ""
+ SaveLog(NameModule, "Проверка порта на: " + ip + ":" + str(port))
+
+ # Проверяем трижды или до 1 успеха
+ while (Attempt < 3):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1)
+ try:
+ sock.connect((ip, int(port)))
+ SaveLog(NameModule, 'CheckPort: Порт ' + str(port) + ' открыт.')
+ # conn.close()
+ return "OPEN"
+ except Exception as E:
+ SaveLog(NameModule, "CheckPort: " + str(E))
+ # LastStatus = "ERROR"
+
+ time.sleep(1)
+ LastStatus = "CLOSE"
+ Attempt = Attempt + 1
+ SaveLog(NameModule, 'CheckPort: Порт ' + str(port) + ' закрыт.')
+ return LastStatus
+
+
+###############################################################################
+def RenameDataSession(SessionID, OldNameVar, NewNameVar) -> bool:
+ """
+ Переименовываем переменную сессии
+ """
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ return False
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ return False
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ os.rename(CurPath + "/" + OldNameVar, CurPath + "/" + NewNameVar)
+ return True
+ return False
+
+
+###############################################################################
+def RemoveFromFolder(folder, RemoveSelf=False) -> str:
+ """
+ Удалить всё из указанной папки
+ * RemoveSelf - Удалить саму папку (уже пустую)
+ return: Текст ошибки
+ """
+
+ print('Обработка папки на удаление : ' + folder)
+ for filename in os.listdir(folder): # Получаем список файлов в папке
+ file_path = os.path.join(folder, filename) # Полный путь к файлу или папке
+ try:
+ if os.path.isfile(file_path): # Если это файл
+ print('Удаляем файл: ' + file_path)
+ os.remove(file_path) # Удалить
+ elif os.path.isdir(file_path): # Если это папка
+ RemoveFromFolder(file_path) # Удаляем содержимое этой папки (рекурсия)
+ print('Удаляем папку : ' + file_path)
+ os.rmdir(file_path) # А затем удалить саму эту папку
+ except Exception as e:
+ TextLog = f'Не удалось удалить {file_path}. Причина: {e}'
+ SaveLog(NameModule, TextLog)
+ return TextLog
+ if (RemoveSelf == True):
+ print('Удаляем папку : ' + folder)
+ os.rmdir(folder) # А затем удалить саму эту папку
+ print('Обработка папки на удаление завершена: ' + folder)
+ return ""
+
+
+###############################################################################
+def RemoveDataSession(SessionID, Variable):
+ """
+ Удаляем переменную сессии
+ """
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "") == False):
+ return False
+ if (os.path.exists("/dev/shm/" + app.config['SHMNAME'] + "/sessions") == False):
+ return False
+
+ # Проверяем наличие сессии
+ CurPath = "/dev/shm/" + app.config['SHMNAME'] + "/sessions/" + SessionID
+ if (os.path.exists(CurPath) == True):
+ if (os.path.isfile(CurPath + "/" + Variable)): os.remove(CurPath + "/" + Variable)
+ return True
+ return False
+
+
+########################################################################
+def GetSHA1_FromFile(Filename):
+ """Хэш для совместимости с MSSQL"""
+ if (os.path.exists(Filename) == False): return ""
+
+ # Расчет хэша
+ file = open(Filename, "rb")
+ CurStr = file.read()
+ file.close()
+
+ hash_object = hashlib.sha1(CurStr)
+ hex_dig = hash_object.hexdigest()
+
+ return hex_dig
+
+
+########################################################################
+def ConvertStrToDate(date_text: str) -> datetime.datetime:
+ """
+ Проверка что строка является датой.
+ Поддерживается ГГГГММДД, ГГГГ-ММ-ДД, ДД.ММ.ГГГГ.
+ """
+ try:
+
+ date_text = date_text.strip()
+ # Если слишком мало текста для преобразования
+ # Дата не указана полностью
+ if (len(date_text) < 8 or len(date_text) == 9): return None # type: ignore
+
+ # время не указано полностью
+ if (len(date_text) > 10 and len(date_text) < 19): return None # type: ignore
+
+ # не 1-х и не 2-х тысячалетие
+ if (len(date_text) == 8 and date_text[:1] not in ('1', '2')): return None # type: ignore
+
+ # ГГГГММДД
+ if (len(date_text) == 8):
+ return datetime.datetime.strptime(date_text, '%Y%m%d')
+
+ # Если точка идет 3-им символов (для формата ДД.ММ.ГГГГ )
+ if (date_text.find('.') == 2 or date_text.find('/') == 2 or date_text.find('-') == 2):
+ date_text = f"{date_text[6:10]}-{date_text[3:5]}-{date_text[0:2]}"
+
+ if (len(date_text) == 10):
+ return datetime.datetime.strptime(date_text.replace(".", "-"), '%Y-%m-%d')
+
+ # Проверка что есть доли секунды
+ if (date_text.find(".") < 0):
+ # Нет
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S')
+ else:
+ # Есть
+ return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S.%f')
+ except Exception:
+ if (date_text == "None"):
+ return None # type: ignore
+ else:
+ return date_text # type: ignore
+
+
+###############################################################################
+def ConvertDate(d: datetime.datetime, NewType=102) -> str:
+ """
+ Преобразование даты в указанный формат
+ 102 - ГГГГ-ММ-ДД
+ 104 - ДД.ММ.ГГГГ
+ 107 - январь 2023
+ 108 - Январь 2023
+ 109 - ЯНВАРЬ 2023
+ """
+ M = {}
+ M.update({"01": "январь"})
+ M.update({"02": "февраль"})
+ M.update({"03": "март"})
+ M.update({"04": "апрель"})
+ M.update({"05": "май"})
+ M.update({"06": "июнь"})
+ M.update({"07": "июль"})
+ M.update({"08": "август"})
+ M.update({"09": "сентябрь"})
+ M.update({"10": "октябрь"})
+ M.update({"11": "ноябрь"})
+ M.update({"12": "декабрь"})
+
+ if NewType == 112:
+ return d.strftime("%Y%m%d")
+ if NewType == 102:
+ return d.strftime("%Y-%m-%d")
+ if NewType == 104:
+ return d.strftime("%d.%m.%Y")
+
+ if NewType == 107:
+ return M[d.strftime("%m")] + " " + d.strftime("%Y")
+ if NewType == 108:
+ return M[d.strftime("%m")].capitalize() + " " + d.strftime("%Y")
+ if NewType == 109:
+ return M[d.strftime("%m")].upper() + " " + d.strftime("%Y")
+ return ""
+
+
+###############################################################################
+def GetTZ():
+ """Получить текущий часовой пояс"""
+
+ now = datetime.datetime.now()
+ Z = str(now.replace(tzinfo=datetime.timezone.utc) - now.astimezone(datetime.timezone.utc)).split(':')[0]
+ return int(Z)
+
+
+###############################################################################
+def GetStatusService(ServiceName) -> str:
+ # Проверка статуса
+ Result = subprocess.run("systemctl -all | grep " + ServiceName, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+
+ if (str(Result.returncode) == "1"): return "None"
+
+ # Проверка статуса
+ p = subprocess.Popen(["systemctl", "is-active", ServiceName], stdout=subprocess.PIPE)
+ (output, err) = p.communicate()
+ output = output.decode('utf-8')
+ # print(output)
+ return output.strip()
+
+
+###############################################################################
+def CreateDBSettings():
+ """Создание базы и таблицы настроек"""
+
+ # Проверяем и создаем путь для локальных сессий
+ if (os.path.exists("db") == False):
+ os.mkdir("db")
+ if (os.path.exists("db/Settings") == False):
+ os.mkdir("db/Settings")
+
+
+###############################################################################
+def GetFromRequestsT(Url: str, Attempt=1, headers={}) -> str:
+ """Получить ответ по HTTP/S ссылке"""
+
+ if (headers == {}):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем GET-запрос на URL
+ response = requests.get(Url, headers=headers, timeout=600, verify=CheckCert)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return GetFromRequestsT(Url, Attempt + 1)
+ else:
+ return ""
+
+ # Возвращаем тело ответа в виде строки
+ return response.text
+
+
+###############################################################################
+def GetFromRequestsB(Url: str, Attempt=1, headers={}) -> bytes:
+ """
+ Получить ответ по HTTP/S ссылке.
+ Ответ в байтах. Например, скачаный двоичный файл.
+ """
+
+ if (headers == {}):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем GET-запрос на URL
+ response = requests.get(Url, headers=headers, timeout=600, verify=CheckCert)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return GetFromRequestsB(Url, Attempt + 1)
+ else:
+ return None
+
+ # Или в виде байтов
+ return response.content
+
+
+###############################################################################
+def PostFromRequestsB(Url: str, Data, Attempt=1, headers={}) -> bytes:
+ """
+ Получить ответ по HTTP/S ссылке.
+ Ответ в байтах. Например, скачаный двоичный файл.
+ """
+
+ if (headers == {}):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем POST-запрос на URL
+ response = requests.post(Url, headers=headers, timeout=600, verify=CheckCert, data=Data)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return PostFromRequestsB(Url, Data, Attempt + 1)
+ else:
+ return None
+
+ # Или в виде байтов
+ return response.content
+
+
+###############################################################################
+def PostFromRequestsT(Url: str, data, Attempt=1, headers={}) -> str:
+ """
+ Получить ответ по HTTP/S ссылке.
+ Ответ в виде текста.
+ """
+ if (headers == {}):
+ headers = {
+ 'Accept': '*/*',
+
+ 'Accept-Language': 'ru,en;q=0.9',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.142 Safari/537.36',
+ }
+
+ # Сайты-исключения где не надо проверять сертификат.
+ CheckCert = True
+
+ if (Url.find('opendata.digital.gov.ru') > 0):
+ CheckCert = False # Игнорировать или нет проверку сертификата для этого сайта
+
+ # При работе с локальными адресами используем свой сертификат
+ if (Url.find('192.168.') > 0 or Url.find('10.') > 0 or Url.find('127.0.0.1') > 0):
+ CheckCert = "app/static/CA"
+
+ # Отправляем POST-запрос на URL
+ response = requests.post(Url, headers=headers, timeout=600, verify=CheckCert, data=data)
+
+ # Получаем статус-код ответа
+ status_code = response.status_code
+
+ if (str(status_code)[0] == "5"):
+ # Если получен код ошибки 502, повторяем запрос до 3 раз
+ if (Attempt <= 3):
+ return PostFromRequestsT(Url, data, Attempt + 1)
+ else:
+ return ""
+
+ # Или в виде текста
+ return response.text
+
+
+###############################################################################
+def DayWeek(CurDate: datetime.datetime = datetime.datetime.today()):
+ "Получить день недели (по умолчанию - сегодня)"
+ weekday = CurDate.weekday()
+ return weekday + 1 # Что бы было от 1 до 7
diff --git a/app/source/OLD_Pages.py b/app/source/OLD_Pages.py
new file mode 100644
index 0000000..c7f1fb2
--- /dev/null
+++ b/app/source/OLD_Pages.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Дата оптимизации: 09.06.2022
+Дата тестирования: 14.06.2022
+"""
+import os
+
+# Активация FLASK
+from app import app # чтение из config.py
+from flask import render_template # использование шаблонов
+# from flask import request # получение данных Cookie, GET и POST
+from flask import jsonify # ответ в формате JSON
+# from flask import redirect # Код страницы и перенаравление
+# import json
+import sys
+import configparser
+import importlib.util
+
+# Подключаем свои библиотеки
+sys.path.append("app/source")
+import API_Common as APIC
+import API_App
+# import API_MSSQL
+# import API_App
+
+
+###############################################################################
+# Проверка сессии
+###############################################################################
+def CheckSession(SessionID):
+ if SessionID is not None:
+ if (SessionID != ""):
+ CurDataSession: APIC.DataSession = APIC.CheckSession(SessionID, True)
+ if (CurDataSession.status == False): return False
+
+ CurSessionID = CurDataSession.sessionid
+
+ # Если нет в базе такой сессии, то заного авторизуемся
+ if (CurSessionID == ""): return False
+
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
+###############################################################################
+# POST
+###############################################################################
+def API(SessionID, CurPage):
+ config = configparser.ConfigParser()
+ config.read("app/pages/" + CurPage + "/config.ini")
+
+ VarArray = {}
+ if (CheckSession(SessionID) == False) and config["ACCESS"]["NeedAuth"] == 1:
+ return "Требуется повторная авторизация. Нажмите F5.", 401
+
+ if (CurPage != ""):
+ spec = importlib.util.spec_from_file_location("module.name", f"app/pages/{CurPage}/source.py")
+ CurPage = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(CurPage)
+ return CurPage.API(SessionID)
+
+ VarArray.update({'status': 'Неизвестная операция'})
+ return jsonify(VarArray)
+
+
+###############################################################################
+# MAIN
+###############################################################################
+def Main(SessionID, CurPage):
+ # Объявление переменных
+ Var = {"Title": "", "ProgName": app.config['PROGNAME']}
+ Modal_Vars = {}
+
+ S = APIC.CheckSession(SessionID, False)
+
+ # Получаем уровень доступа
+ if (S.userid not in ("-1", "")):
+ UserData: API_MSSQL.Result = API_App.GetDataUserByID(APIC.ReadDataSession(SessionID, "userid"))
+
+ # Создаем страничку
+ try:
+ if (CurPage is None):
+ return render_template('Pages.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserHeader(SessionID, Var) # Рисуем шапку на странице
+ , Modals=APIC.Modals(Modal_Vars) # Модальные окна
+ , ThreadVars_message=APIC.ThreadVars_Message() #
+ , version=app.config['VERSION'] #
+ , R_category="{{R.category}}" #
+ , R_name="{{R.name}}" #
+ , R_description="{{R.description}}" #
+ , R_autor="{{R.autor}}" #
+ , R_tags="{{R.tags}}" #
+ , CurTag="{{CurTag}}" #
+ )
+
+ # Если вошли в режиме Установщика то проверку не проверяем
+ if (S.userid != "-1"):
+ # Проверка прав на модуль
+ if (os.path.isfile("app/pages/" + CurPage + "/config.ini") == False):
+ return render_template('error_403.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserErrorHeader(SessionID, Var) # Рисуем шапку на странице
+ , ThreadVars_message=f"Не удалось получить настройки для страницы {CurPage}. Похоже, эта страница на стадии разработки")
+
+ config = configparser.ConfigParser()
+ config.read("app/pages/" + CurPage + "/config.ini")
+
+ if (config["ACCESS"]["UserInterFace"] not in ("0", "")):
+ if (
+ str(UserData.GetRecord(0, "UserInterFace")) not in config["ACCESS"]["UserInterFace"].split(',')
+ # or AccessOMS < int(config["ACCESS"]["AccessOMS"])
+ or int(UserData.GetRecord(0, "AccessOMS")) < int(config["ACCESS"]["AccessOMS"])
+ or int(UserData.GetRecord(0, "AccessLPU")) < int(config["ACCESS"]["AccessLPU"])
+ or int(UserData.GetRecord(0, "AccessUOG")) < int(config["ACCESS"]["AccessUOG"])
+ or int(UserData.GetRecord(0, "AccessENP")) < int(config["ACCESS"]["AccessENP"])
+ or int(UserData.GetRecord(0, "AccessSMS")) < int(config["ACCESS"]["AccessSMS"])
+ or int(UserData.GetRecord(0, "AccessPostCard")) < int(config["ACCESS"]["AccessPostCard"])
+ or int(UserData.GetRecord(0, "AccessPhone")) < int(config["ACCESS"]["AccessPhone"])
+ or int(UserData.GetRecord(0, "AccessDial")) < int(config["ACCESS"]["AccessDial"])
+ or int(UserData.GetRecord(0, "AccessAnkets")) < int(config["ACCESS"]["AccessAnkets"])
+
+ ):
+ return render_template('error_403.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserErrorHeader(SessionID, Var) # Рисуем шапку на странице
+ , ThreadVars_message="Извините, но вам не хватает доступа к этой странице.")
+
+ import importlib.util
+ spec = importlib.util.spec_from_file_location("module.name", "app/pages/" + CurPage + "/source.py")
+ CurPage = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(CurPage)
+ return CurPage.Main(SessionID)
+ except Exception as E:
+ return render_template('error_500.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserHeader(SessionID, Var) # Рисуем шапку на странице
+ , ThreadVars_message=f"При открытии страницы {CurPage} произошла ошибка: {E}")
diff --git a/app/source/Pages.py b/app/source/Pages.py
new file mode 100644
index 0000000..5a94d85
--- /dev/null
+++ b/app/source/Pages.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Дата оптимизации: 09.06.2022
+Дата тестирования: 14.06.2022
+"""
+import os
+
+# Активация FLASK
+from app import app # чтение из config.py
+from flask import render_template # использование шаблонов
+# from flask import request # получение данных Cookie, GET и POST
+from flask import jsonify # ответ в формате JSON
+# from flask import redirect # Код страницы и перенаравление
+# import json
+import sys
+import configparser
+import importlib.util
+
+# Подключаем свои библиотеки
+sys.path.append("app/source")
+import API_Common as APIC
+import API_App
+
+
+
+###############################################################################
+# Проверка сессии
+###############################################################################
+def CheckSession(SessionID):
+ if SessionID is not None:
+ if (SessionID != ""):
+ CurDataSession: APIC.DataSession = APIC.CheckSession(SessionID, True)
+ if (CurDataSession.status == False): return False
+
+ CurSessionID = CurDataSession.sessionid
+
+ # Если нет в базе такой сессии, то заного авторизуемся
+ if (CurSessionID == ""): return False
+
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
+###############################################################################
+# POST
+###############################################################################
+def API(SessionID, CurPage):
+ config = configparser.ConfigParser()
+ config.read("app/pages/" + CurPage + "/config.ini")
+
+ VarArray = {}
+ if (CheckSession(SessionID) == False) and config["ACCESS"]["NeedAuth"] == 1:
+ return "Требуется повторная авторизация. Нажмите F5.", 401
+
+ if (CurPage != ""):
+ spec = importlib.util.spec_from_file_location("module.name", f"app/pages/{CurPage}/source.py")
+ CurPage = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(CurPage)
+ return CurPage.API(SessionID)
+
+ VarArray.update({'status': 'Неизвестная операция'})
+ return jsonify(VarArray)
+
+
+###############################################################################
+# MAIN
+###############################################################################
+def Main(SessionID, CurPage):
+ # Объявление переменных
+ Var = {"Title": "", "ProgName": app.config['PROGNAME']}
+ Modal_Vars = {}
+
+ S = APIC.CheckSession(SessionID, False)
+
+ # Получаем уровень доступа
+ if (S.userid not in ("-1", "")):
+ pass # Пока не проверяем
+
+ # Создаем страничку
+ try:
+ if (CurPage is None):
+ return render_template('Pages.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserHeader(SessionID, Var) # Рисуем шапку на странице
+ , Modals=APIC.Modals(Modal_Vars) # Модальные окна
+ , ThreadVars_message=APIC.ThreadVars_Message() #
+ , version=app.config['VERSION'] #
+ , R_category="{{R.category}}" #
+ , R_name="{{R.name}}" #
+ , R_description="{{R.description}}" #
+ , R_autor="{{R.autor}}" #
+ , R_tags="{{R.tags}}" #
+ , CurTag="{{CurTag}}" #
+ )
+
+ # Если вошли в режиме Установщика то проверку не проверяем
+ if (S.userid != "-1"):
+ # Проверка прав на модуль
+ if (os.path.isfile("app/pages/" + CurPage + "/config.ini") == False):
+ return render_template('error_403.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserErrorHeader(SessionID, Var) # Рисуем шапку на странице
+ , ThreadVars_message=f"Не удалось получить настройки для страницы {CurPage}. Похоже, эта страница на стадии разработки")
+
+ config = configparser.ConfigParser()
+ config.read("app/pages/" + CurPage + "/config.ini")
+
+ if (config["ACCESS"]["UserInterFace"] not in ("0", "")):
+ if (
+
+
+ ):
+ return render_template('error_403.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserErrorHeader(SessionID, Var) # Рисуем шапку на странице
+ , ThreadVars_message="Извините, но вам не хватает доступа к этой странице.")
+
+ import importlib.util
+ spec = importlib.util.spec_from_file_location("module.name", "app/pages/" + CurPage + "/source.py")
+ CurPage = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(CurPage)
+ return CurPage.Main(SessionID)
+ except Exception as E:
+ return render_template('error_500.htm', head=APIC.UserHead(Var) # Подключаем раздел head со скриптами и css
+ , UserHeader=APIC.UserHeader(SessionID, Var) # Рисуем шапку на странице
+ , ThreadVars_message=f"При открытии страницы {CurPage} произошла ошибка: {E}")
diff --git a/app/static/image/rustdesk/rd1.png b/app/static/image/rustdesk/rd1.png
index 87b76e1481386414af9aa83ab05a09c44c007894..dd359c2276919c9641cb9b2b2e125761382c9d59 100644
GIT binary patch
literal 60601
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