added clipcascade

This commit is contained in:
liph
2026-07-23 16:48:58 +02:00
parent 90e44adbae
commit cba86d9dce
31 changed files with 6406 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
import base64
import json
import hashlib
from Crypto.Cipher import AES
from core.constants import *
from core.config import Config
class CipherManager:
def __init__(self, config: Config):
self.config = config
# hash
self.hash_name = "sha256"
self.dklen = 32 # 256 bits for AES-256
# encryption
self.mode = AES.MODE_GCM
def hash_password(self, password: str) -> bytes:
return hashlib.pbkdf2_hmac(
hash_name=self.hash_name,
password=password.encode(),
salt=(
self.config.data["username"] + password + self.config.data["salt"]
).encode("utf-8"),
iterations=self.config.data["hash_rounds"],
dklen=self.dklen,
)
def encrypt(self, plaintext: str) -> dict:
key = self.config.data["hashed_password"]
plaintext_bytes = plaintext.encode("utf-8")
cipher = AES.new(key, self.mode)
ciphertext, tag = cipher.encrypt_and_digest(plaintext_bytes)
return {"nonce": cipher.nonce, "ciphertext": ciphertext, "tag": tag}
def decrypt(self, nonce: bytes, ciphertext: bytes, tag: bytes) -> str:
key = self.config.data["hashed_password"]
cipher = AES.new(key, self.mode, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag).decode()
@staticmethod
def encode_to_json_string(**kwargs: bytes) -> str:
"""
Convert bytes values to Base64 and create a JSON string.
Args:
**kwargs: Key-value pairs where values must be of type `bytes`.
Returns:
str: A JSON string with all `bytes` values Base64-encoded.
Raises:
ValueError: If a value is not of type `bytes`.
"""
json_data = {}
for key, value in kwargs.items():
if isinstance(value, bytes):
json_data[key] = base64.b64encode(value).decode("utf-8")
else:
raise ValueError(
f"Unsupported value type for key '{key}': {type(value)}. "
f"This method only supports 'bytes'."
)
return json.dumps(json_data)
@staticmethod
def decode_from_json_string(json_string: str) -> dict:
"""
Decode a JSON string where all values are Base64-encoded back to their original bytes.
Args:
json_string (str): A JSON string with Base64-encoded values.
Returns:
dict: A dictionary with the original keys and `bytes` values decoded from Base64.
Raises:
ValueError: If the JSON string is not valid or if decoding fails.
"""
# Parse the JSON string into a dictionary
json_data = json.loads(json_string)
decoded_data = {}
# Decode each Base64-encoded value back to bytes
for key, value in json_data.items():
if isinstance(value, str):
decoded_data[key] = base64.b64decode(value)
else:
raise ValueError(
f"Unsupported value type for key '{key}': {type(value)}. "
+ f"Expected 'str' for Base64 decoding."
)
return decoded_data
@staticmethod
def string_to_sha3_512_lowercase_hex(input_string: str) -> str:
"""
Convert a string to its lowercase hexadecimal SHA3-512 hash.
Args:
input_string (str): The input string to hash.
Returns:
str: The lowercase hexadecimal representation of the SHA3-512 hash.
"""
return hashlib.sha3_512(input_string.encode("utf-8")).hexdigest()
+42
View File
@@ -0,0 +1,42 @@
from core.constants import *
from core.config import Config
if PLATFORM == WINDOWS or (
PLATFORM.startswith(LINUX) and not LINUX_USE_CLI_UI
):
# WINDOWS: When creating the executable with pyinstaller, add --hidden-import plyer.platforms.win.notification
from plyer import notification
elif PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
from cli.info import CustomDialog
elif PLATFORM == MACOS:
import subprocess
class NotificationManager:
def __init__(self, config: Config):
self.config = config
def notify(self, title: str, message: str, app_name: str = APP_NAME, timeout=10):
if self.config.data["notification"]: # Check if notifications are enabled
try:
if PLATFORM == WINDOWS or (
PLATFORM.startswith(LINUX) and not LINUX_USE_CLI_UI
):
notification.notify(
title=title,
message=message,
app_name=app_name,
timeout=timeout, # seconds
)
elif PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
CustomDialog(f"{title} : {message}").mainloop()
elif PLATFORM == MACOS:
subprocess.run(
[
"osascript",
"-e",
f'display notification "{message}" with title "{title}"',
]
)
except Exception as e:
logging.error(f"Failed to handle notification: {e}")
+201
View File
@@ -0,0 +1,201 @@
import json
import logging
import requests
from core.constants import *
from core.config import Config
from bs4 import BeautifulSoup
from utils.ssl_helper import requests_verify_arg
class RequestManager:
def __init__(self, config: Config):
self.config = config
def _verify(self):
return requests_verify_arg(self.config)
@staticmethod
def format_cookie(cookie: dict) -> str:
"""
Format the cookie string for headers.
"""
return f"JSESSIONID={cookie.get('JSESSIONID', '')};"
def login(self) -> tuple[bool, str, dict]:
try:
session = requests.Session()
# Fetch the login page to get the CSRF token
response = session.get(
self.config.data["server_url"] + LOGIN_URL,
verify=self._verify(),
)
if response.status_code != 200:
msg = f"Failed to fetch login page: {response.status_code}"
logging.error(msg)
return False, msg, None
soup = BeautifulSoup(response.text, "html.parser")
csrf_token = soup.find("input", {"name": "_csrf"})["value"]
# Login with the credentials
form_data = {
"username": self.config.data["username"],
"password": self.config.data["password"],
"_csrf": csrf_token,
}
response = session.post(
self.config.data["server_url"] + LOGIN_URL,
data=form_data,
verify=self._verify(),
)
if (
response.status_code == 200
and "bad credentials" not in response.text.lower()
):
# login successful
cookie = session.cookies.get_dict()
logging.info(f"Login successful: {response.status_code}")
return True, "Login successful", cookie
else:
# login failed
msg = f"Login failed: {response.status_code}"
logging.error(msg)
return False, msg, None
except Exception as e:
msg = f"An error occurred during login: {e}"
logging.error(msg)
return False, msg, None
def maxsize(self) -> int:
try:
response = RequestManager.get(
url=self.config.data["server_url"] + MAXSIZE_URL,
headers={
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
},
verify=self._verify(),
)
if response.status_code == 200:
# maxsize request successful
maxsize = response.json().get("maxsize", MAX_SIZE)
logging.info(f"Max size: {maxsize}")
return maxsize
except Exception as e:
logging.error(
f"Error fetching max size: {e}, defaulting to {MAX_SIZE} Bytes"
)
return MAX_SIZE
def get_server_mode(self) -> str:
try:
response = RequestManager.get(
url=self.config.data["server_url"] + SERVER_MODE_URL,
headers={
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
},
verify=self._verify(),
)
if response.status_code == 200:
# server mode request successful
server_mode = response.json().get("mode")
logging.info(f"Server mode: {server_mode}")
return server_mode
except Exception as e:
logging.error(f"Error fetching server mode: {e}")
raise
def get_stun_url(self) -> str:
try:
response = RequestManager.get(
url=self.config.data["server_url"] + STUN_URL,
headers={
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
},
verify=self._verify(),
)
if response.status_code == 200:
# stun url request successful
stun_url = response.json().get("url")
logging.info(f"STUN URL: {stun_url}")
return stun_url
except Exception as e:
logging.error(f"Error fetching STUN URL: {e}")
raise
def get_metadata(self) -> dict:
try:
response = RequestManager.get(
url=METADATA_URL,
headers={
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
},
verify=True,
)
if response.status_code == 200:
# metadata request successful
return response.json()
except Exception as e:
logging.error(f"Error fetching metadata: {e}")
raise
def logout(self):
try:
response = RequestManager.post(
url=self.config.data["server_url"] + LOGOUT_URL,
data={"_csrf": self.config.data["csrf_token"]},
headers={
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
},
verify=self._verify(),
)
if response.status_code == 204:
logging.info(f"Logout successful: {response.status_code}")
except Exception as e:
logging.error(f"Error during logout: {e}")
def get_csrf_token(self) -> str:
try:
response = RequestManager.get(
url=self.config.data["server_url"] + CSRF_URL,
headers={
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
},
verify=self._verify(),
)
if response.status_code == 200:
# CSRF token request successful
return json.loads(response.text).get("token", "")
except Exception as e:
logging.error(f"Error fetching CSRF token: {e}")
return ""
@staticmethod
def get(url: str, headers: dict = None, verify=True) -> requests.Response:
"""
A generic GET mapper for handling GET requests.
"""
try:
response = requests.get(url, headers=headers, verify=verify)
response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code
return response
except Exception as e:
logging.error(f"Error during GET request to {url}: {e}")
raise
@staticmethod
def post(
url: str, data: dict, headers: dict = None, verify=True
) -> requests.Response:
"""
A generic POST mapper for handling POST requests.
"""
try:
response = requests.post(url, data=data, headers=headers, verify=verify)
response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code
return response
except Exception as e:
logging.error(f"Error during POST request to {url}: {e}")
raise
+32
View File
@@ -0,0 +1,32 @@
import os
import ssl
from typing import Any, Dict, Optional, Union
from core.constants import MACOS, PLATFORM
if PLATFORM == MACOS:
import certifi
def requests_verify_arg(config) -> Union[bool, str]:
"""Argument for requests' verify= when calling the user's server."""
path = (config.data.get("ssl_ca_bundle") or "").strip()
if not path:
return True
if not os.path.isfile(path):
raise FileNotFoundError(f"SSL CA bundle file not found: {path}")
return path
def websocket_sslopt_for_config(config) -> Optional[Dict[str, Any]]:
"""sslopt for websocket_client run_forever; None uses the library/OS default."""
path = (config.data.get("ssl_ca_bundle") or "").strip()
if path:
if not os.path.isfile(path):
raise FileNotFoundError(f"SSL CA bundle file not found: {path}")
ctx = ssl.create_default_context(cafile=path)
return {"context": ctx}
if PLATFORM == MACOS:
ctx = ssl.create_default_context(cafile=certifi.where())
return {"context": ctx}
return None
+45
View File
@@ -0,0 +1,45 @@
import logging
import re
from core.constants import *
def center_window(win, max_height=None):
"""Center a Tk/Toplevel window on the primary monitor."""
logging.debug(f"Centering window with max_height={max_height}")
win.update_idletasks()
window_width = win.winfo_width()
window_height = win.winfo_height()
if max_height:
window_height = min(window_height, max_height)
logging.debug(f"Window dimensions: {window_width}x{window_height}")
primary_x, primary_y = 0, 0
primary_w, primary_h = win.winfo_screenwidth(), win.winfo_screenheight()
try:
if PLATFORM.startswith(LINUX):
logging.debug("Detecting monitors on Linux platform")
import subprocess
result = subprocess.run(
["xrandr", "--query"], capture_output=True, text=True, timeout=2
)
for line in result.stdout.splitlines():
if " connected primary" in line:
m = re.search(r"(\d+)x(\d+)\+(\d+)\+(\d+)", line)
if m:
primary_w = int(m.group(1))
primary_h = int(m.group(2))
primary_x = int(m.group(3))
primary_y = int(m.group(4))
logging.debug(
f"Primary monitor: position=({primary_x}, {primary_y}), size={primary_w}x{primary_h}"
)
break
except Exception as e:
logging.warning(f"Failed to detect primary monitor: {e}. Using fallback dimensions.")
x = primary_x + (primary_w - window_width) // 2
y = primary_y + (primary_h - window_height) // 2
x = max(primary_x, x)
y = max(primary_y, y)
logging.debug(f"Calculated window position: ({x}, {y})")
win.geometry(f"{window_width}x{window_height}+{x}+{y}")
logging.debug(f"Applied geometry: {window_width}x{window_height}+{x}+{y}")