webtools-cli 1.2.5__tar.gz → 1.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.5
3
+ Version: 1.2.6
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
@@ -54,7 +54,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
54
54
 
55
55
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
56
56
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
57
- - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
57
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage with **Seamless Mono-Installer** (one-click, conflict-free setup).
58
58
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
59
59
  - **💻 Terminal-First**: Designed for power users who live in the command line.
60
60
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -16,7 +16,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
16
16
 
17
17
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
18
18
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
19
- - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
19
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage with **Seamless Mono-Installer** (one-click, conflict-free setup).
20
20
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
21
21
  - **💻 Terminal-First**: Designed for power users who live in the command line.
22
22
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "webtools-cli"
7
- version = "1.2.5"
7
+ version = "1.2.6"
8
8
  description = "Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,62 +7,24 @@ if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
7
7
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
8
8
  except Exception: pass
9
9
 
10
- # --- AUTO-INSTALLER FOR MISSING/BROKEN DEPENDENCIES ---
10
+ # --- AUTO-INSTALLER FOR PLAYWRIGHT BROWSERS ---
11
11
  def ensure_dependencies():
12
- """Performs an aggressive forced repair of the environment if issues are detected"""
13
- # Dependencies to verify (module_name -> (pip_pkg, min_v, max_v))
14
- mandatory = {
15
- "mega": ("mega.py", None, None),
16
- "Crypto": ("pycryptodome", None, None),
17
- "tenacity": ("tenacity>=8.2.3,<9.0.0", "8", "9")
18
- }
19
-
20
- needs_repair = False
21
-
22
- # 1. Check for corrupted Crypto namespace (Mixed pycrypto/pycryptodome)
12
+ """Ensures Playwright browsers are installed if the library is present"""
23
13
  try:
24
- from Crypto.PublicKey import RSA
25
- except (SyntaxError, AttributeError, ImportError):
26
- needs_repair = True
27
-
28
- # 2. Check for missing or outdated modules
29
- if not needs_repair:
30
- for module, (pkg, min_v, max_v) in mandatory.items():
14
+ from playwright.sync_api import sync_playwright
15
+ # Simple check for browser existence
16
+ with sync_playwright() as p:
31
17
  try:
32
- mod = __import__(module)
33
- if min_v or max_v:
34
- from importlib.metadata import version as get_v
35
- v_str = get_v(module)
36
- major = int(v_str.split('.')[0])
37
- if (min_v and major < int(min_v)) or (max_v and major >= int(max_v)):
38
- needs_repair = True; break
18
+ p.chromium.launch(headless=True).close()
39
19
  except:
40
- needs_repair = True; break
41
-
42
- if needs_repair:
43
- print("\n[!] Environment instability detected (Dependency Conflict).")
44
- print("[*] Performing Aggressive Environment Repair (Python 3.11+ Stability Patch)...")
45
- try:
46
- # A: Deep clean the Crypto and Tenacity namespaces
47
- # We uninstall everything that could possibly clash
48
- bad_pkgs = ["pycrypto", "pycryptodome", "pycryptodomex", "tenacity", "mega.py"]
49
- subprocess.call([sys.executable, "-m", "pip", "uninstall", "-y"] + bad_pkgs,
50
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
51
-
52
- # B: Forced Reinstall of the MODERN stack
53
- # We use --force-reinstall to ensure no broken files remain
54
- print("[*] Rebuilding core libraries...")
55
- subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "--force-reinstall",
56
- "pycryptodome", "tenacity>=8.2.3,<9.0.0", "requests"])
57
-
58
- # C: Install mega.py WITHOUT its own dependencies (to prevent it from breaking tenacity again)
59
- subprocess.check_call([sys.executable, "-m", "pip", "install", "mega.py", "--no-deps"])
60
-
61
- print("[+] Environment successfully repaired! Please restart the app.\n")
62
- sys.exit(0) # Exit to allow the user to restart with the fresh environment
63
- except Exception as e:
64
- print(f"[-] Auto-repair failed: {e}")
65
- print("[!] Please run: pip uninstall -y pycrypto pycryptodome tenacity mega.py && pip install pycryptodome tenacity>=8.2.3,<9.0.0 mega.py --no-deps")
20
+ print("\n[*] Playwright found but browsers are missing. Auto-installing...")
21
+ subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"])
22
+ except ImportError:
23
+ # pip install handled this, but just in case
24
+ pass
25
+ except Exception as e:
26
+ # Non-critical failure, don't block startup
27
+ pass
66
28
 
67
29
  ensure_dependencies()
68
30
 
@@ -85,174 +47,12 @@ from collections import Counter
85
47
  from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file, after_this_request
86
48
  from PIL import Image,ExifTags,ImageChops,ImageEnhance
87
49
  from io import BytesIO
88
- from mega import Mega
89
- try:
90
- from mega.errors import RequestError
91
- from mega.crypto import (
92
- base64_to_a32, decrypt_key, base64_url_decode, a32_to_str,
93
- encrypt_key, str_to_a32, mpi_to_int, base64_url_encode
94
- )
95
- from Crypto.PublicKey import RSA
96
- import binascii
97
- except ImportError:
98
- class RequestError(Exception): pass
99
-
100
- # --- MONKEY PATCH FOR MEGA.PY (Fixes TypeError in AES calls) ---
101
- try:
102
- from Crypto.Cipher import AES
103
- import mega.mega as mega_module
104
- _original_aes_new = AES.new
105
-
106
- def _patched_aes_new(key, *args, **kwargs):
107
- # Convert key to bytes if string
108
- if isinstance(key, str):
109
- key = key.encode('latin-1')
110
-
111
- # Convert positional arguments (like IV/mode) to bytes if strings
112
- new_args = list(args)
113
- for i in range(len(new_args)):
114
- if isinstance(new_args[i], str):
115
- new_args[i] = new_args[i].encode('latin-1')
116
-
117
- # Convert keyword arguments (IV, nonce, etc.) to bytes if strings
118
- for k in kwargs:
119
- if isinstance(kwargs[k], str):
120
- kwargs[k] = kwargs[k].encode('latin-1')
121
-
122
- return _original_aes_new(key, *new_args, **kwargs)
123
-
124
- # Apply patch globally
125
- AES.new = _patched_aes_new
126
- if hasattr(mega_module, 'AES'):
127
- mega_module.AES.new = _patched_aes_new
128
- except Exception:
129
- pass
130
-
131
- class RobustMega(Mega):
132
- """
133
- Patched version of Mega library to handle fragile API responses.
134
- The original library crashes if the API returns non-JSON or an empty string.
135
- """
136
- def _login_process(self, resp, password):
137
- """
138
- Overridden to fix 'Invalid RSA public exponent' error in newer pycryptodome.
139
- The original code passes 0 as the exponent, which is rejected.
140
- """
141
- encrypted_master_key = base64_to_a32(resp['k'])
142
- self.master_key = decrypt_key(encrypted_master_key, password)
143
- if 'tsid' in resp:
144
- tsid = base64_url_decode(resp['tsid'])
145
- key_encrypted = a32_to_str(
146
- encrypt_key(str_to_a32(tsid[:16]), self.master_key)
147
- )
148
- if key_encrypted == tsid[-16:]:
149
- self.sid = resp['tsid']
150
- elif 'csid' in resp:
151
- encrypted_rsa_private_key = base64_to_a32(resp['privk'])
152
- rsa_private_key = decrypt_key(
153
- encrypted_rsa_private_key, self.master_key
154
- )
155
-
156
- private_key_str = a32_to_str(rsa_private_key)
157
- self.rsa_private_key = [0, 0, 0, 0]
158
-
159
- # This loop parses the 4 components: p, q, d, u
160
- for i in range(4):
161
- l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
162
- self.rsa_private_key[i] = mpi_to_int(private_key_str[:l])
163
- private_key_str = private_key_str[l:]
164
-
165
- encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
166
-
167
- # The Fix: Calculate the real 'e' from p, q, d
168
- p = self.rsa_private_key[0]
169
- q = self.rsa_private_key[1]
170
- d = self.rsa_private_key[2]
171
- n = p * q
172
- phi = (p - 1) * (q - 1)
173
-
174
- try:
175
- # Calculate real e = d^-1 mod phi
176
- e = pow(d, -1, phi)
177
- rsa_decrypter = RSA.construct((n, e, d, p, q))
178
- except:
179
- # Fallback to 65537 if inverse fails (shouldn't happen)
180
- e = 65537
181
- rsa_decrypter = RSA.construct((n, e, d, p, q), consistency_check=False)
182
-
183
- # Handle potential library differences in how the key is accessed
184
- key_obj = rsa_decrypter.key if hasattr(rsa_decrypter, 'key') else rsa_decrypter
185
-
186
- # Decrypt sid integer
187
- sid_int = key_obj._decrypt(encrypted_sid)
188
-
189
- # Convert to hex then to bytes, ensuring we handle the 0 padding if any
190
- sid_hex = '%x' % sid_int
191
- if len(sid_hex) % 2:
192
- sid_hex = '0' + sid_hex
193
- sid_bytes = binascii.unhexlify(sid_hex)
194
-
195
- self.sid = base64_url_encode(sid_bytes[:43])
196
-
197
- def _api_request(self, data):
198
- params = {'id': self.sequence_num}
199
- self.sequence_num += 1
200
50
 
201
- if self.sid:
202
- params.update({'sid': self.sid})
203
-
204
- if not isinstance(data, list):
205
- data = [data]
206
-
207
- url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
208
- headers = {
209
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
210
- 'Accept': '*/*',
211
- 'Accept-Language': 'en-US,en;q=0.9',
212
- 'Origin': 'https://mega.nz',
213
- 'Referer': 'https://mega.nz/'
214
- }
215
-
216
- try:
217
- req = requests.post(
218
- url,
219
- params=params,
220
- data=json.dumps(data),
221
- headers=headers,
222
- timeout=self.timeout,
223
- )
224
-
225
- content = req.text.strip() if req.text else ""
226
-
227
- if not content:
228
- if req.status_code == 402:
229
- raise Exception("Mega.nz is temporarily blocking this connection (Status 402). This usually happens due to rate-limiting. Please try again later or use a different network.")
230
- raise Exception(f"Mega API returned empty response (Status {req.status_code})")
231
-
232
- try:
233
- json_resp = json.loads(content)
234
- except json.JSONDecodeError:
235
- # Try to parse as integer error code
236
- try:
237
- val = int(content)
238
- raise RequestError(val)
239
- except (ValueError, RequestError):
240
- if content.startswith("-") and content[1:].isdigit():
241
- raise RequestError(int(content))
242
- raise Exception(f"Mega API error: {content[:100]}")
243
-
244
- if isinstance(json_resp, int):
245
- if json_resp == -3:
246
- time.sleep(0.5)
247
- return self._api_request(data=data)
248
- raise RequestError(json_resp)
249
-
250
- return json_resp[0]
251
- except Exception:
252
- raise
51
+ # Internal Mega Client (Conflict-free and Python 3.11+ compatible)
52
+ from .install import Mega, RequestError
253
53
 
254
54
  # Global Mega session state
255
- mega_engine = RobustMega()
55
+ mega_engine = Mega()
256
56
  mega_sessions = {} # Mapping: email -> {'client': MegaClient, 'info': user_info, 'quota': quota}
257
57
  active_mega_email = None
258
58
 
@@ -0,0 +1,168 @@
1
+ """Installation and Utility Utilities for WebTools CLI"""
2
+ import os
3
+ import sys
4
+ import json
5
+ import random
6
+ import binascii
7
+ import requests
8
+ import subprocess
9
+ from Crypto.Cipher import AES
10
+ from Crypto.PublicKey import RSA
11
+ from Crypto.Util import Counter
12
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
13
+
14
+ # --- PLAYWRIGHT SETUP ---
15
+
16
+ def install_playwright_browsers():
17
+ """Install Playwright Chromium browser after package installation"""
18
+ try:
19
+ print("📦 Installing Playwright Chromium browser...")
20
+ subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
21
+ print("✓ Playwright Chromium installed successfully")
22
+ except subprocess.CalledProcessError:
23
+ print("⚠️ Failed to install Playwright browsers. Run manually: playwright install chromium")
24
+ except Exception as e:
25
+ print(f"⚠️ Playwright browser installation skipped: {e}")
26
+
27
+ # --- INTERNAL MEGA CLIENT (Conflict-free & Python 3.11+ compatible) ---
28
+
29
+ class RequestError(Exception):
30
+ """Exception for Mega API requests"""
31
+ pass
32
+
33
+ def base64_url_decode(data):
34
+ data += '=' * (4 - len(data) % 4)
35
+ return binascii.a2b_base64(data.replace('-', '+').replace('_', '/'))
36
+
37
+ def base64_url_encode(data):
38
+ return binascii.b2a_base64(data).decode('utf-8').strip().replace('+', '-').replace('/', '_').replace('=', '')
39
+
40
+ def a32_to_str(a):
41
+ return binascii.unhexlify(''.join(format(i, '08x') for i in a))
42
+
43
+ def str_to_a32(s):
44
+ if len(s) % 4:
45
+ s += b'\0' * (4 - len(s) % 4)
46
+ return [int(binascii.hexlify(s[i:i + 4]), 16) for i in range(0, len(s), 4)]
47
+
48
+ def base64_to_a32(s):
49
+ return str_to_a32(base64_url_decode(s))
50
+
51
+ def a32_to_base64(a):
52
+ return base64_url_encode(a32_to_str(a))
53
+
54
+ def aes_cbc_encrypt(data, key):
55
+ cipher = AES.new(a32_to_str(key), AES.MODE_CBC, b'\0' * 16)
56
+ return str_to_a32(cipher.encrypt(a32_to_str(data)))
57
+
58
+ def aes_cbc_decrypt(data, key):
59
+ cipher = AES.new(a32_to_str(key), AES.MODE_CBC, b'\0' * 16)
60
+ return str_to_a32(cipher.decrypt(a32_to_str(data)))
61
+
62
+ def decrypt_key(a, k):
63
+ res = []
64
+ for i in range(0, len(a), 4):
65
+ res += aes_cbc_decrypt(a[i:i + 4], k)
66
+ return res
67
+
68
+ def encrypt_key(a, k):
69
+ res = []
70
+ for i in range(0, len(a), 4):
71
+ res += aes_cbc_encrypt(a[i:i + 4], k)
72
+ return res
73
+
74
+ def prepare_key(a):
75
+ v = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
76
+ for _ in range(0x10000):
77
+ for j in range(0, len(a), 4):
78
+ key = [0, 0, 0, 0]
79
+ for k in range(4):
80
+ if j + k < len(a):
81
+ key[k] = a[j + k]
82
+ v = aes_cbc_encrypt(v, key)
83
+ return v
84
+
85
+ def mpi_to_int(s):
86
+ return int(binascii.hexlify(s[2:]), 16)
87
+
88
+ class Mega:
89
+ def __init__(self):
90
+ self.sid = None
91
+ self.api_url = "https://g.api.mega.co.nz/cs"
92
+ self.sequence_num = random.randint(0, 0xFFFFFFFF)
93
+
94
+ def login(self, email, password):
95
+ password_aes = prepare_key(str_to_a32(password.encode('utf-8')))
96
+ user_hash = self._stringhash(email, password_aes)
97
+ resp = self._api_request({'a': 'us', 'user': email, 'uh': user_hash})
98
+
99
+ if isinstance(resp, int) and resp < 0:
100
+ raise RequestError(f"Login failed: {resp}")
101
+
102
+ encrypted_key = base64_to_a32(resp['k'])
103
+ self.master_key = decrypt_key(encrypted_key, password_aes)
104
+
105
+ if 'tsid' in resp:
106
+ self.sid = resp['tsid']
107
+ elif 'csid' in resp:
108
+ encrypted_rsa_private_key = base64_to_a32(resp['privk'])
109
+ rsa_private_key = decrypt_key(encrypted_rsa_private_key, self.master_key)
110
+ private_key_str = a32_to_str(rsa_private_key)
111
+ components = [0, 0, 0, 0]
112
+ for i in range(4):
113
+ l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
114
+ components[i] = mpi_to_int(private_key_str[:l])
115
+ private_key_str = private_key_str[l:]
116
+ encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
117
+ p, q, d, _ = components
118
+ n, phi = p * q, (p - 1) * (q - 1)
119
+ try: e = pow(d, -1, phi)
120
+ except: e = 65537
121
+ rsa_decrypter = RSA.construct((n, e, d, p, q))
122
+ sid_int = rsa_decrypter._decrypt(encrypted_sid)
123
+ sid_hex = '%x' % sid_int
124
+ if len(sid_hex) % 2: sid_hex = '0' + sid_hex
125
+ self.sid = base64_url_encode(binascii.unhexlify(sid_hex)[:43])
126
+ return self
127
+
128
+ def _stringhash(self, email, aes_key):
129
+ s = email.encode('utf-8')
130
+ h = [0, 0, 0, 0]
131
+ for i, b in enumerate(s): h[i % 4] ^= b
132
+ for _ in range(0x4000): h = aes_cbc_encrypt(h, aes_key)
133
+ return a32_to_base64([h[0], h[2]])
134
+
135
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10),
136
+ retry=retry_if_exception_type(requests.exceptions.RequestException))
137
+ def _api_request(self, data):
138
+ url = f"{self.api_url}?id={self.sequence_num}"
139
+ if self.sid: url += f"&sid={self.sid}"
140
+ self.sequence_num += 1
141
+ response = requests.post(url, json=[data], timeout=30)
142
+ response.raise_for_status()
143
+ result = response.json()
144
+ if isinstance(result, list):
145
+ res = result[0]
146
+ if isinstance(res, int) and res < 0: raise RequestError(f"API Error {res}")
147
+ return res
148
+ return result
149
+
150
+ def get_storage_space(self):
151
+ resp = self._api_request({'a': 'uq', 'strg': 1})
152
+ return {'total': resp['mstrg'], 'used': resp['cstrg']}
153
+
154
+ def upload(self, filename, dest_folder=None):
155
+ size = os.path.getsize(filename)
156
+ resp = self._api_request({'a': 'u', 's': size})
157
+ upload_url = resp['p']
158
+ with open(filename, 'rb') as f: data = f.read()
159
+ r = requests.post(upload_url, data=data, timeout=60)
160
+ upload_token = r.text
161
+ file_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)]
162
+ attribs = {'n': os.path.basename(filename)}
163
+ encoded_attribs = base64_url_encode(b'MEGA' + json.dumps(attribs).encode('utf-8'))
164
+ data = {'a': 'p', 't': dest_folder or 'ROOT', 'n': [{'h': upload_token, 't': 0, 'a': encoded_attribs, 'k': a32_to_base64(file_key[:4])}]}
165
+ return self._api_request(data)
166
+
167
+ if __name__ == "__main__":
168
+ install_playwright_browsers()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.5
3
+ Version: 1.2.6
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
@@ -54,7 +54,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
54
54
 
55
55
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
56
56
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
57
- - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
57
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage with **Seamless Mono-Installer** (one-click, conflict-free setup).
58
58
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
59
59
  - **💻 Terminal-First**: Designed for power users who live in the command line.
60
60
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -1,17 +0,0 @@
1
- """Post-install script to setup Playwright browsers"""
2
- import subprocess
3
- import sys
4
-
5
- def install_playwright_browsers():
6
- """Install Playwright Chromium browser after package installation"""
7
- try:
8
- print("📦 Installing Playwright Chromium browser...")
9
- subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
10
- print("✓ Playwright Chromium installed successfully")
11
- except subprocess.CalledProcessError:
12
- print("⚠️ Failed to install Playwright browsers. Run manually: playwright install chromium")
13
- except Exception as e:
14
- print(f"⚠️ Playwright browser installation skipped: {e}")
15
-
16
- if __name__ == "__main__":
17
- install_playwright_browsers()
File without changes
File without changes