vzrecon 3.0.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.
vzrecon-3.0.0/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VZLOI GROUP
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
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
vzrecon-3.0.0/PKG-INFO ADDED
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: vzrecon
3
+ Version: 3.0.0
4
+ Summary: Protocol-level validator for HTTP/SOCKS4/SOCKS5 proxies
5
+ Author: VZLOI GROUP
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/vzloi/vzrecon
8
+ Project-URL: Source, https://github.com/vzloi/vzrecon
9
+ Project-URL: Issues, https://github.com/vzloi/vzrecon/issues
10
+ Project-URL: Changelog, https://github.com/vzloi/vzrecon/blob/main/CHANGELOG.md
11
+ Keywords: proxy,socks,socks5,socks4,validator,recon,async
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet
22
+ Classifier: Topic :: System :: Networking
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # vzrecon
33
+
34
+ Protocol-level validator for HTTP, SOCKS4 and SOCKS5 proxies.
35
+ Zero dependencies. Termux-native.
36
+
37
+ Signed: VZLOI GROUP // ops
38
+
39
+ ## Install
40
+
41
+ pip install -e .
42
+ vzrecon --self-test
43
+
44
+ ## Use
45
+
46
+ vzrecon # scan cwd for *_proxies.txt
47
+ vzrecon -d ./lists -o ./out # explicit paths
48
+ vzrecon --preset stealth # 200 workers
49
+ vzrecon --preset turbo # 2400 workers
50
+ vzrecon --only http,socks5
51
+ vzrecon --plain
52
+
53
+ ## Protocols
54
+
55
+ - HTTP RFC 7231 CONNECT + forward GET fallback
56
+ - SOCKS5 RFC 1928, no-auth greeting
57
+ - SOCKS4 original spec, IPv4 targets only
58
+
59
+ ## Output
60
+
61
+ - `<proto>_alive.txt` — endpoints, one per line
62
+ - `<proto>_alive.json` — signed_by, protocol, count, proxies[]
63
+ - `<proto>_alive.csv` — endpoint, latency_ms
64
+
65
+ ## Exit codes
66
+
67
+ - 0 scan completed
68
+ - 1 self-test failed
69
+ - 2 config error
70
+ - 3 no source files found
71
+
72
+ ## License
73
+
74
+ MIT. Copyright (c) 2026 VZLOI GROUP.
@@ -0,0 +1,43 @@
1
+ # vzrecon
2
+
3
+ Protocol-level validator for HTTP, SOCKS4 and SOCKS5 proxies.
4
+ Zero dependencies. Termux-native.
5
+
6
+ Signed: VZLOI GROUP // ops
7
+
8
+ ## Install
9
+
10
+ pip install -e .
11
+ vzrecon --self-test
12
+
13
+ ## Use
14
+
15
+ vzrecon # scan cwd for *_proxies.txt
16
+ vzrecon -d ./lists -o ./out # explicit paths
17
+ vzrecon --preset stealth # 200 workers
18
+ vzrecon --preset turbo # 2400 workers
19
+ vzrecon --only http,socks5
20
+ vzrecon --plain
21
+
22
+ ## Protocols
23
+
24
+ - HTTP RFC 7231 CONNECT + forward GET fallback
25
+ - SOCKS5 RFC 1928, no-auth greeting
26
+ - SOCKS4 original spec, IPv4 targets only
27
+
28
+ ## Output
29
+
30
+ - `<proto>_alive.txt` — endpoints, one per line
31
+ - `<proto>_alive.json` — signed_by, protocol, count, proxies[]
32
+ - `<proto>_alive.csv` — endpoint, latency_ms
33
+
34
+ ## Exit codes
35
+
36
+ - 0 scan completed
37
+ - 1 self-test failed
38
+ - 2 config error
39
+ - 3 no source files found
40
+
41
+ ## License
42
+
43
+ MIT. Copyright (c) 2026 VZLOI GROUP.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vzrecon"
7
+ version = "3.0.0"
8
+ description = "Protocol-level validator for HTTP/SOCKS4/SOCKS5 proxies"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{name = "VZLOI GROUP"}]
13
+ keywords = ["proxy","socks","socks5","socks4","validator","recon","async"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: System Administrators",
19
+ "Operating System :: POSIX",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Internet",
25
+ "Topic :: System :: Networking",
26
+ "Topic :: Utilities",
27
+ ]
28
+ dependencies = []
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/vzloi/vzrecon"
35
+ Source = "https://github.com/vzloi/vzrecon"
36
+ Issues = "https://github.com/vzloi/vzrecon/issues"
37
+ Changelog = "https://github.com/vzloi/vzrecon/blob/main/CHANGELOG.md"
38
+
39
+ [project.scripts]
40
+ vzrecon = "vzrecon.cli:main"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["vzrecon*"]
44
+
45
+ [tool.setuptools.package-data]
46
+ vzrecon = ["py.typed"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from vzrecon.config import Config, PRESETS
6
+
7
+ def _write(tmp_path, body):
8
+ p = Path(tmp_path) / "c.toml"
9
+ p.write_text(body)
10
+ return p
11
+
12
+ def test_defaults():
13
+ Config().validate()
14
+
15
+ def test_toml(tmp_path):
16
+ p = _write(tmp_path, 'workers = 500\ntarget_port = 8080\n')
17
+ c = Config.from_toml(p)
18
+ assert c.workers == 500
19
+ assert c.target_port == 8080
20
+
21
+ def test_unknown(tmp_path):
22
+ p = _write(tmp_path, 'nope = 1\n')
23
+ with pytest.raises(ValueError):
24
+ Config.from_toml(p)
25
+
26
+ def test_bad_port():
27
+ with pytest.raises(ValueError):
28
+ Config(target_port=0).validate()
29
+
30
+ def test_bad_format():
31
+ with pytest.raises(ValueError):
32
+ Config(format="xml").validate()
33
+
34
+ def test_preset():
35
+ c = Config().apply_preset("stealth")
36
+ assert c.workers == PRESETS["stealth"]["workers"]
37
+
38
+ def test_bad_preset():
39
+ with pytest.raises(ValueError):
40
+ Config().apply_preset("nope")
@@ -0,0 +1,29 @@
1
+ from vzrecon.metrics import Meter, fmt_dur, fmt_int
2
+
3
+ def test_tick():
4
+ m = Meter()
5
+ m.tick(ok=True, latency_ms=10)
6
+ m.tick(ok=False)
7
+ m.tick(ok=True, latency_ms=20)
8
+ assert m.done == 3
9
+ assert m.ok == 2
10
+ assert m.fail == 1
11
+
12
+ def test_p95():
13
+ m = Meter()
14
+ for v in range(100):
15
+ m.tick(ok=True, latency_ms=float(v))
16
+ assert m.percentile(0.5) == 50
17
+ assert m.percentile(0.95) == 95
18
+
19
+ def test_progress():
20
+ m = Meter()
21
+ m.total = 10
22
+ for _ in range(4):
23
+ m.tick(ok=True)
24
+ assert m.progress == 0.4
25
+
26
+ def test_fmt():
27
+ assert fmt_int(1234567) == "1 234 567"
28
+ assert fmt_dur(None) == "--:--"
29
+ assert fmt_dur(65) == "01:05"
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ import contextlib
3
+
4
+ import pytest
5
+
6
+ from vzrecon.protocols import (
7
+ http_connect,
8
+ socks4_connect,
9
+ socks5_connect,
10
+ )
11
+ from vzrecon.selftest import _mh, _m4, _m5
12
+
13
+ async def _serve(h):
14
+ srv = await asyncio.start_server(h, "127.0.0.1", 0)
15
+ return srv, srv.sockets[0].getsockname()[1]
16
+
17
+ async def _run(handler, validator):
18
+ srv, port = await _serve(handler)
19
+ try:
20
+ return await validator(
21
+ "127.0.0.1", port, "example.com", 443,
22
+ tcp_timeout=2.0, hs_timeout=2.0,
23
+ )
24
+ finally:
25
+ srv.close()
26
+ with contextlib.suppress(Exception):
27
+ await srv.wait_closed()
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_http():
31
+ lat = await _run(_mh, http_connect)
32
+ assert lat is not None and lat > 0
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_socks5():
36
+ lat = await _run(_m5, socks5_connect)
37
+ assert lat is not None and lat > 0
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_socks4():
41
+ lat = await _run(_m4, socks4_connect)
42
+ assert lat is not None and lat > 0
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_refused():
46
+ lat = await http_connect(
47
+ "127.0.0.1", 1, "example.com", 443,
48
+ tcp_timeout=0.5, hs_timeout=0.5,
49
+ )
50
+ assert lat is None
@@ -0,0 +1,4 @@
1
+ __version__="3.0.0"
2
+ __codename__="IRON VEIL"
3
+ __author__="VZLOI GROUP"
4
+ __all__=["Config","Scan","Meter","Hit"]
@@ -0,0 +1,3 @@
1
+ import sys
2
+ from .cli import main
3
+ sys.exit(main())
@@ -0,0 +1,42 @@
1
+ BANNER = (
2
+ " ██╗ ██╗███████╗██╗ ██████╗ ██╗",
3
+ " ██║ ██║╚══███╔╝██║ ██╔═══██╗██║",
4
+ " ██║ ██║ ███╔╝ ██║ ██║ ██║██║",
5
+ " ╚██╗ ██╔╝ ███╔╝ ██║ ██║ ██║██║",
6
+ " ╚████╔╝ ███████╗███████╗╚██████╔╝██║",
7
+ " ╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═╝",
8
+ )
9
+ CREW = "VZLOI GROUP"
10
+ SUFFIX = "ops"
11
+ TOOL = "vzrecon"
12
+ CODENAME = "IRON VEIL"
13
+ TAGLINE = "recon division"
14
+
15
+ class SGR:
16
+ R = "\x1b[0m"
17
+ B = "\x1b[1m"
18
+ D = "\x1b[2m"
19
+ I = "\x1b[3m"
20
+ U = "\x1b[4m"
21
+
22
+ BLK = "\x1b[38;5;232m"
23
+ IRON = "\x1b[38;5;240m"
24
+ SLATE = "\x1b[38;5;244m"
25
+ ASH = "\x1b[38;5;250m"
26
+ IVORY = "\x1b[38;5;255m"
27
+
28
+ BLOOD = "\x1b[38;5;52m"
29
+ CRIM = "\x1b[38;5;160m"
30
+ FLARE = "\x1b[38;5;203m"
31
+ GOLD = "\x1b[38;5;220m"
32
+ AMBER = "\x1b[38;5;214m"
33
+
34
+ CYAN = "\x1b[38;5;66m"
35
+ ICE = "\x1b[38;5;117m"
36
+ VERD = "\x1b[38;5;77m"
37
+ GREEN = "\x1b[38;5;114m"
38
+
39
+ # aliases for legacy call sites
40
+ DIM = IRON
41
+ STEEL = SLATE
42
+ GRN = VERD
@@ -0,0 +1,131 @@
1
+ import argparse, asyncio, contextlib, secrets, sys, time
2
+ from pathlib import Path
3
+ from . import __codename__, __version__
4
+ from .brand import BANNER, CREW, SGR, SUFFIX, TOOL
5
+ from .config import PRESETS, Config
6
+ from .engine import Scan, discover
7
+ from .platform import notify, wake_lock, wake_unlock
8
+ from .tui import TUI
9
+ from .writers import emit_csv, emit_json, emit_txt
10
+ def _parse(a):
11
+ p=argparse.ArgumentParser(prog=TOOL,
12
+ description=f"proxy validator // {CREW} {SUFFIX}")
13
+ p.add_argument("--version",action="version",
14
+ version=f"{TOOL} {__version__} ({__codename__})")
15
+ p.add_argument("-c","--config",type=Path)
16
+ p.add_argument("-d","--dir",type=Path,default=Path.cwd())
17
+ p.add_argument("-o","--output-dir",type=Path,default=Path("."))
18
+ p.add_argument("-t","--target")
19
+ p.add_argument("-w","--workers",type=int)
20
+ p.add_argument("--format",choices=("txt","json","csv"))
21
+ p.add_argument("--only")
22
+ p.add_argument("--preset",choices=tuple(PRESETS.keys()))
23
+ p.add_argument("--list-presets",action="store_true")
24
+ p.add_argument("--self-test",action="store_true")
25
+ p.add_argument("--plain",action="store_true")
26
+ p.add_argument("--no-notify",action="store_true")
27
+ p.add_argument("--verify",action="store_true",
28
+ help="send a real request through the tunnel")
29
+ return p.parse_args(a)
30
+ def _merge(a):
31
+ c=Config.from_toml(a.config) if a.config else Config()
32
+ if a.preset: c=c.apply_preset(a.preset)
33
+ c.source_dir=str(a.dir); c.output_dir=str(a.output_dir)
34
+ if a.workers is not None: c.workers=a.workers
35
+ if a.format: c.format=a.format
36
+ if a.target:
37
+ h,_,pt=a.target.partition(":")
38
+ c.target_host=h; c.target_port=int(pt) if pt else 443
39
+ if a.only:
40
+ c.protocols=tuple(s.strip() for s in a.only.split(",") if s.strip())
41
+ if a.no_notify: c.notify=False
42
+ if a.verify: c.verify=True
43
+ c.validate(); return c
44
+ async def _dispatch(scans,cfg,plain):
45
+ if plain:
46
+ await asyncio.gather(*(s.run() for s in scans)); return
47
+ sid=secrets.token_hex(4).upper()
48
+ tui=TUI(scans,f"{cfg.target_host}:{cfg.target_port}",
49
+ cfg.workers,sid,"balanced")
50
+ tui.attach(); tui.enter()
51
+ ts=[asyncio.create_task(s.run()) for s in scans]
52
+ tick=1.0/cfg.render_hz
53
+ try:
54
+ while not all(t.done() for t in ts):
55
+ for s in scans: s.meter.sample()
56
+ tui.draw(); await asyncio.sleep(tick)
57
+ tui.draw()
58
+ finally:
59
+ for t in ts: t.cancel()
60
+ with contextlib.suppress(Exception):
61
+ await asyncio.gather(*ts,return_exceptions=True)
62
+ tui.leave()
63
+ def _write(scans,cfg,out):
64
+ out.mkdir(parents=True,exist_ok=True)
65
+ for s in scans:
66
+ stem=out/f"{s.kind}_alive"
67
+ if cfg.format=="txt": emit_txt(stem.with_suffix(".txt"),s.hits)
68
+ elif cfg.format=="json": emit_json(stem.with_suffix(".json"),s.kind,s.hits)
69
+ else: emit_csv(stem.with_suffix(".csv"),s.hits)
70
+ def _banner():
71
+ return "\n".join(f" {SGR.CRIM}{l}{SGR.R}" for l in BANNER)
72
+ def _presets():
73
+ print(f"\n {SGR.CRIM}{SGR.B}{CREW} // {SUFFIX}{SGR.R} · presets")
74
+ print(f" {SGR.DIM}{'─'*46}{SGR.R}")
75
+ for n,v in PRESETS.items():
76
+ print(f" {SGR.GOLD}{n:<10}{SGR.R} {SGR.STEEL}"
77
+ f"workers={v['workers']:<5} tcp={v['tcp_timeout']}s "
78
+ f"hs={v['hs_timeout']}s hz={v['render_hz']}{SGR.R}")
79
+ print(); return 0
80
+ def main(argv=None):
81
+ a=_parse(argv)
82
+ if a.list_presets: return _presets()
83
+ if a.self_test:
84
+ from .selftest import run
85
+ print(f"\n {SGR.CRIM}{SGR.B}{CREW} // {SUFFIX}{SGR.R} "
86
+ f"· protocol self-test")
87
+ print(f" {SGR.DIM}{'─'*46}{SGR.R}")
88
+ rc=asyncio.run(run()); print(); return rc
89
+ try: cfg=_merge(a)
90
+ except (ValueError,FileNotFoundError) as e:
91
+ print(f"{SGR.CRIM}config:{SGR.R} {e}",file=sys.stderr)
92
+ return 2
93
+ src=discover(cfg)
94
+ if not src:
95
+ print(f"{SGR.CRIM}no sources{SGR.R} in {cfg.source_dir}",
96
+ file=sys.stderr)
97
+ return 3
98
+ print(_banner())
99
+ print(f" {SGR.STEEL}protocols:{SGR.R} {', '.join(k for k,_ in src)}")
100
+ print(f" {SGR.STEEL}sources: {SGR.R} {len(src)} file(s)")
101
+ print(f" {SGR.STEEL}target: {SGR.R} {cfg.target_host}:{cfg.target_port}")
102
+ print(f" {SGR.STEEL}workers: {SGR.R} {cfg.workers}\n")
103
+ if cfg.wake_lock: wake_lock()
104
+ scans=[]
105
+ Path(cfg.output_dir).mkdir(parents=True,exist_ok=True)
106
+ for k,s in src:
107
+ scans.append(Scan(k,s,Path(cfg.output_dir)/f"{k}_alive.txt",cfg))
108
+ t0=time.monotonic()
109
+ try:
110
+ with contextlib.suppress(KeyboardInterrupt):
111
+ asyncio.run(_dispatch(scans,cfg,a.plain))
112
+ finally:
113
+ if cfg.wake_lock: wake_unlock()
114
+ dt=time.monotonic()-t0
115
+ _write(scans,cfg,Path(cfg.output_dir))
116
+ print(f"\n {SGR.CRIM}{SGR.B}{CREW} // {SUFFIX}{SGR.R} "
117
+ f"· scan complete")
118
+ print(f" {SGR.DIM}{'─'*46}{SGR.R}")
119
+ print(f" {SGR.STEEL}duration:{SGR.R} {dt:.1f}s")
120
+ total=0
121
+ for s in scans:
122
+ m=s.meter
123
+ pct=(m.ok/m.done*100) if m.done else 0
124
+ total+=m.ok
125
+ print(f" {s.kind:<7} {m.ok:>6} / {m.done:<6} ({pct:5.1f}%)")
126
+ print(f" {SGR.DIM}{'─'*46}{SGR.R}")
127
+ print(f" total {SGR.GRN}{total}{SGR.R} alive → {cfg.output_dir}\n")
128
+ if cfg.notify:
129
+ notify(f"{TOOL} · scan complete",
130
+ f"{total} alive · {sum(s.meter.done for s in scans)} checked · {dt:.0f}s")
131
+ return 0
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+ import tomllib
3
+ from dataclasses import dataclass,fields,replace
4
+ from pathlib import Path
5
+ PRESETS={
6
+ "stealth": {"workers":200,"tcp_timeout":4.0,"hs_timeout":8.0,"render_hz":2.0},
7
+ "balanced":{"workers":1200,"tcp_timeout":3.0,"hs_timeout":6.0,"render_hz":3.0},
8
+ "turbo": {"workers":2400,"tcp_timeout":2.0,"hs_timeout":5.0,"render_hz":4.0},
9
+ }
10
+ @dataclass(slots=True)
11
+ class Config:
12
+ workers:int=1200
13
+ tcp_timeout:float=3.0
14
+ hs_timeout:float=6.0
15
+ target_host:str="api.trongrid.io"
16
+ target_port:int=443
17
+ render_hz:float=3.0
18
+ sample_size:int=10
19
+ source_dir:str="."
20
+ output_dir:str="."
21
+ format:str="txt"
22
+ protocols:tuple=("http","socks4","socks5")
23
+ notify:bool=True
24
+ wake_lock:bool=True
25
+ verify:bool=False
26
+ @classmethod
27
+ def from_toml(cls,path):
28
+ raw=tomllib.loads(Path(path).read_text("utf-8"))
29
+ valid={f.name for f in fields(cls)}
30
+ unknown=set(raw)-valid
31
+ if unknown: raise ValueError(f"unknown keys: {sorted(unknown)}")
32
+ if "protocols" in raw: raw["protocols"]=tuple(raw["protocols"])
33
+ return cls(**raw)
34
+ def apply_preset(self,name):
35
+ if name not in PRESETS: raise ValueError(f"unknown preset: {name}")
36
+ return replace(self,**PRESETS[name])
37
+ def validate(self):
38
+ if self.workers<1: raise ValueError("workers must be >= 1")
39
+ if not (1<=self.target_port<=65535): raise ValueError("target_port out of range")
40
+ if self.format not in {"txt","json","csv"}: raise ValueError(f"bad format: {self.format}")
41
+ for p in self.protocols:
42
+ if p not in {"http","socks4","socks5"}: raise ValueError(f"bad protocol: {p}")
@@ -0,0 +1,66 @@
1
+ import asyncio, contextlib
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from .metrics import Meter
5
+ from .protocols import REGISTRY
6
+ @dataclass(slots=True)
7
+ class Hit:
8
+ endpoint:str
9
+ kind:str
10
+ latency_ms:float
11
+ class Scan:
12
+ def __init__(self,kind,source,sink,cfg,on_hit=None):
13
+ self.kind=kind; self.source=source; self.sink=sink; self.cfg=cfg
14
+ self.on_hit=on_hit; self.meter=Meter()
15
+ self._q=asyncio.Queue(); self._fh=None; self.hits=[]
16
+ def _load(self):
17
+ if not self.source.exists(): return []
18
+ seen=set(); out=[]
19
+ for raw in self.source.read_text("utf-8",errors="ignore").splitlines():
20
+ l=raw.strip()
21
+ if l and l not in seen:
22
+ seen.add(l); out.append(l)
23
+ return out
24
+ async def run(self):
25
+ e=self._load(); self.meter.total=len(e)
26
+ if not e: return
27
+ for x in e: self._q.put_nowait(x)
28
+ self._fh=self.sink.open("w",buffering=1)
29
+ v=REGISTRY[self.kind]; cfg=self.cfg
30
+ async def consume():
31
+ while True:
32
+ try: entry=self._q.get_nowait()
33
+ except asyncio.QueueEmpty: return
34
+ try:
35
+ h,_,ps=entry.rpartition(":")
36
+ if not h or not ps.isdigit():
37
+ self.meter.tick(ok=False); continue
38
+ lat=await v(h,int(ps),cfg.target_host,cfg.target_port,
39
+ tcp_timeout=cfg.tcp_timeout,
40
+ hs_timeout=cfg.hs_timeout,
41
+ verify=cfg.verify)
42
+ if lat is None:
43
+ self.meter.tick(ok=False)
44
+ else:
45
+ self.meter.tick(ok=True,latency_ms=lat)
46
+ self._fh.write(entry+"\n")
47
+ self.hits.append((entry,lat))
48
+ if self.on_hit:
49
+ with contextlib.suppress(Exception):
50
+ self.on_hit(Hit(entry,self.kind,lat))
51
+ except asyncio.CancelledError: return
52
+ except Exception: self.meter.tick(ok=False)
53
+ finally: self._q.task_done()
54
+ ws=[asyncio.create_task(consume()) for _ in range(cfg.workers)]
55
+ try: await asyncio.gather(*ws)
56
+ finally:
57
+ for t in ws: t.cancel()
58
+ await asyncio.gather(*ws,return_exceptions=True)
59
+ if self._fh is not None: self._fh.close()
60
+ def discover(cfg):
61
+ base=Path(cfg.source_dir); found=[]
62
+ for p in cfg.protocols:
63
+ s=base/f"{p}_proxies.txt"
64
+ if s.exists() and s.stat().st_size>0:
65
+ found.append((p,s))
66
+ return found
@@ -0,0 +1,54 @@
1
+ import time
2
+ from collections import deque
3
+ from dataclasses import dataclass,field
4
+ _S="▁▂▃▄▅▆▇█"
5
+ @dataclass
6
+ class Meter:
7
+ total:int=0
8
+ done:int=0
9
+ ok:int=0
10
+ fail:int=0
11
+ started_at:float=field(default_factory=time.monotonic)
12
+ _w:deque=field(default_factory=lambda:deque(maxlen=60))
13
+ _p:int=0
14
+ _l:deque=field(default_factory=lambda:deque(maxlen=512))
15
+ def tick(self,*,ok,latency_ms=None):
16
+ self.done+=1
17
+ if ok:
18
+ self.ok+=1
19
+ if latency_ms is not None: self._l.append(latency_ms)
20
+ else: self.fail+=1
21
+ def sample(self):
22
+ self._w.append((time.monotonic(),self.done-self._p))
23
+ self._p=self.done
24
+ def rate(self):
25
+ if len(self._w)<2: return 0.0
26
+ t0,_=self._w[0]; t1,_=self._w[-1]
27
+ dt=t1-t0
28
+ if dt<=0: return 0.0
29
+ return sum(n for _,n in self._w)/dt
30
+ def eta(self):
31
+ rem=self.total-self.done; r=self.rate()
32
+ if r<=0 or rem<=0: return None
33
+ return rem/r
34
+ def percentile(self,p):
35
+ if not self._l: return None
36
+ d=sorted(self._l)
37
+ return d[min(len(d)-1,int(p*len(d)))]
38
+ def spark(self,w=24):
39
+ if not self._w: return " "*w
40
+ s=[n for _,n in self._w][-w:]
41
+ hi=max(s) or 1
42
+ return "".join(_S[min(7,(n*8)//hi)] for n in s).rjust(w)
43
+ @property
44
+ def elapsed(self): return time.monotonic()-self.started_at
45
+ @property
46
+ def progress(self): return self.done/self.total if self.total else 0.0
47
+ def fmt_dur(s):
48
+ if s is None: return "--:--"
49
+ n=int(s)
50
+ return f"{n//60:02d}:{n%60:02d}" if n<3600 else f"{n//3600}h{(n%3600)//60:02d}m"
51
+ def fmt_int(n): return f"{n:,}".replace(","," ")
52
+ def fmt_uptime(s):
53
+ s=int(s); h,r=divmod(s,3600); m,sec=divmod(r,60)
54
+ return f"{h:02d}:{m:02d}:{sec:02d}"
@@ -0,0 +1,23 @@
1
+ import os, shutil, subprocess
2
+ def _have(c): return shutil.which(c) is not None
3
+ def is_termux(): return "com.termux" in os.environ.get("PREFIX","")
4
+ def wake_lock():
5
+ if not _have("termux-wake-lock"): return False
6
+ try:
7
+ subprocess.run(["termux-wake-lock"],check=False,
8
+ stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
9
+ return True
10
+ except Exception: return False
11
+ def wake_unlock():
12
+ if not _have("termux-wake-unlock"): return
13
+ try:
14
+ subprocess.run(["termux-wake-unlock"],check=False,
15
+ stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
16
+ except Exception: pass
17
+ def notify(t,b):
18
+ if not _have("termux-notification"): return
19
+ try:
20
+ subprocess.run(["termux-notification","--title",t,"--content",b,
21
+ "--priority","high"],check=False,
22
+ stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
23
+ except Exception: pass
@@ -0,0 +1,100 @@
1
+ import asyncio, socket
2
+ from time import perf_counter
3
+ async def _d(h,p,t):
4
+ return await asyncio.wait_for(asyncio.open_connection(h,p),timeout=t)
5
+ async def _tun(host,port,th,tp,tt,ht,verify=False):
6
+ r=w=None; t0=perf_counter()
7
+ try:
8
+ r,w=await _d(host,port,tt)
9
+ req=(f"CONNECT {th}:{tp} HTTP/1.1\r\nHost: {th}:{tp}\r\n"
10
+ f"Proxy-Connection: keep-alive\r\n\r\n")
11
+ w.write(req.encode()); await w.drain()
12
+ line=await asyncio.wait_for(r.readline(),timeout=ht)
13
+ if not line.startswith(b"HTTP/"): return None
14
+ parts=line.split(b" ",2)
15
+ if len(parts)<2 or parts[1]!=b"200": return None
16
+ # ── verify phase: real request through the tunnel ──
17
+ if verify:
18
+ v = await _verify_through(r,w,th,tp,ht)
19
+ if v is None: return None
20
+ return (perf_counter()-t0)*1000.0
21
+ except (OSError,asyncio.TimeoutError,asyncio.IncompleteReadError): return None
22
+ finally:
23
+ if w is not None: w.close()
24
+ async def _verify_through(r,w,th,tp,ht):
25
+ """Send a real HTTP/1.0 request through the established tunnel.
26
+ Any HTTP status line at all means the proxy is actually forwarding."""
27
+ try:
28
+ req=(f"HEAD / HTTP/1.0\r\nHost: {th}\r\n"
29
+ f"User-Agent: vzrecon/3.0\r\n\r\n")
30
+ w.write(req.encode()); await w.drain()
31
+ line=await asyncio.wait_for(r.readline(),timeout=ht)
32
+ if not line.startswith(b"HTTP/"): return None
33
+ return 1
34
+ except (OSError,asyncio.TimeoutError,asyncio.IncompleteReadError):
35
+ return None
36
+ async def _fwd(host,port,th,tt,ht):
37
+ r=w=None; t0=perf_counter()
38
+ try:
39
+ r,w=await _d(host,port,tt)
40
+ req=(f"GET http://{th}/ HTTP/1.1\r\nHost: {th}\r\n"
41
+ f"User-Agent: vzrecon/3.0\r\nConnection: close\r\n\r\n")
42
+ w.write(req.encode()); await w.drain()
43
+ line=await asyncio.wait_for(r.readline(),timeout=ht)
44
+ if not line.startswith(b"HTTP/"): return None
45
+ return (perf_counter()-t0)*1000.0
46
+ except (OSError,asyncio.TimeoutError,asyncio.IncompleteReadError): return None
47
+ finally:
48
+ if w is not None: w.close()
49
+ async def http_connect(host,port,th,tp,*,tcp_timeout,hs_timeout,verify=False):
50
+ r=await _tun(host,port,th,tp,tcp_timeout,hs_timeout,verify=verify)
51
+ if r is not None: return r
52
+ # forward-GET is already a real request; no extra verify needed
53
+ return await _fwd(host,port,th,tcp_timeout,hs_timeout)
54
+ async def socks5_connect(host,port,th,tp,*,tcp_timeout,hs_timeout,verify=False):
55
+ r=w=None; t0=perf_counter()
56
+ try:
57
+ r,w=await _d(host,port,tcp_timeout)
58
+ w.write(b"\x05\x01\x00"); await w.drain()
59
+ g=await asyncio.wait_for(r.readexactly(2),timeout=hs_timeout)
60
+ if g!=b"\x05\x00": return None
61
+ n=th.encode("idna")
62
+ if len(n)>255: return None
63
+ w.write(b"\x05\x01\x00\x03"+bytes((len(n),))+n+tp.to_bytes(2,"big"))
64
+ await w.drain()
65
+ h=await asyncio.wait_for(r.readexactly(4),timeout=hs_timeout)
66
+ if h[0]!=0x05 or h[1]!=0x00: return None
67
+ a=h[3]
68
+ if a==0x01: tail=6
69
+ elif a==0x04: tail=18
70
+ elif a==0x03:
71
+ ln=await asyncio.wait_for(r.readexactly(1),timeout=hs_timeout)
72
+ tail=ln[0]+2
73
+ else: return None
74
+ await asyncio.wait_for(r.readexactly(tail),timeout=hs_timeout)
75
+ if verify:
76
+ v = await _verify_through(r,w,th,tp,hs_timeout)
77
+ if v is None: return None
78
+ return (perf_counter()-t0)*1000.0
79
+ except (OSError,asyncio.TimeoutError,asyncio.IncompleteReadError): return None
80
+ finally:
81
+ if w is not None: w.close()
82
+ async def socks4_connect(host,port,th,tp,*,tcp_timeout,hs_timeout,verify=False):
83
+ r=w=None; t0=perf_counter()
84
+ try:
85
+ try: ipv4=socket.gethostbyname(th)
86
+ except OSError: return None
87
+ octets=bytes(int(x) for x in ipv4.split("."))
88
+ r,w=await _d(host,port,tcp_timeout)
89
+ w.write(b"\x04\x01"+tp.to_bytes(2,"big")+octets+b"\x00")
90
+ await w.drain()
91
+ rep=await asyncio.wait_for(r.readexactly(8),timeout=hs_timeout)
92
+ if rep[0]!=0x00 or rep[1]!=0x5A: return None
93
+ if verify:
94
+ v = await _verify_through(r,w,th,tp,hs_timeout)
95
+ if v is None: return None
96
+ return (perf_counter()-t0)*1000.0
97
+ except (OSError,asyncio.TimeoutError,asyncio.IncompleteReadError): return None
98
+ finally:
99
+ if w is not None: w.close()
100
+ REGISTRY={"http":http_connect,"socks4":socks4_connect,"socks5":socks5_connect}
File without changes
@@ -0,0 +1,54 @@
1
+ import asyncio, contextlib
2
+ from .protocols import http_connect, socks4_connect, socks5_connect
3
+ async def _mh(r,w):
4
+ try:
5
+ l=await r.readline()
6
+ if l.startswith(b"CONNECT"):
7
+ w.write(b"HTTP/1.1 200 Connection established\r\n\r\n")
8
+ await w.drain()
9
+ while True:
10
+ h=await r.readline()
11
+ if h in (b"\r\n",b"\n",b""): break
12
+ finally: w.close()
13
+ async def _m5(r,w):
14
+ try:
15
+ g=await r.readexactly(2)
16
+ if g!=b"\x05\x01": return
17
+ await r.readexactly(1)
18
+ w.write(b"\x05\x00"); await w.drain()
19
+ h=await r.readexactly(4); a=h[3]
20
+ if a==0x01: await r.readexactly(6)
21
+ elif a==0x03:
22
+ n=(await r.readexactly(1))[0]
23
+ await r.readexactly(n+2)
24
+ elif a==0x04: await r.readexactly(18)
25
+ w.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
26
+ await w.drain()
27
+ finally: w.close()
28
+ async def _m4(r,w):
29
+ try:
30
+ await r.readexactly(8)
31
+ w.write(b"\x00\x5a\x00\x00\x00\x00\x00\x00")
32
+ await w.drain()
33
+ finally: w.close()
34
+ CASES=[("http",_mh,http_connect),
35
+ ("socks5",_m5,socks5_connect),
36
+ ("socks4",_m4,socks4_connect)]
37
+ async def _serve(h):
38
+ srv=await asyncio.start_server(h,"127.0.0.1",0)
39
+ return srv,srv.sockets[0].getsockname()[1]
40
+ async def run():
41
+ fail=0
42
+ for name,handler,v in CASES:
43
+ srv,port=await _serve(handler)
44
+ try:
45
+ lat=await v("127.0.0.1",port,"example.com",443,
46
+ tcp_timeout=2.0,hs_timeout=2.0)
47
+ ok=lat is not None
48
+ print(f" {name:<7} {'ok ' if ok else 'FAIL'} {lat or 0:>7.2f} ms")
49
+ if not ok: fail+=1
50
+ finally:
51
+ srv.close()
52
+ with contextlib.suppress(Exception):
53
+ await srv.wait_closed()
54
+ return 0 if fail==0 else 1
@@ -0,0 +1,206 @@
1
+ import shutil, sys
2
+ from datetime import datetime, timezone
3
+ from .brand import BANNER, CODENAME, CREW, SUFFIX, TAGLINE, TOOL, SGR
4
+ from .metrics import fmt_dur, fmt_int, fmt_uptime
5
+
6
+ def _vis(s):
7
+ n = 0; i = 0
8
+ while i < len(s):
9
+ if s[i] == "\x1b":
10
+ j = s.find("m", i)
11
+ if j == -1: break
12
+ i = j + 1; continue
13
+ n += 1; i += 1
14
+ return n
15
+
16
+ def _pad(s, w):
17
+ v = _vis(s)
18
+ return s if v >= w else s + " " * (w - v)
19
+
20
+ def _hr(l, c, r, ch, w):
21
+ return f"{SGR.BLOOD}{l}{ch * w}{r}{SGR.R}"
22
+
23
+ def _box_top(title, w):
24
+ t = f" {title} "
25
+ pad = w - _vis(t) - 2
26
+ return f"{SGR.BLOOD}┌─{r}{SGR.CRIM}{SGR.B}{t}{SGR.R}{SGR.BLOOD}{'─' * pad}─┐{SGR.R}"
27
+
28
+ def _box_bot(w):
29
+ return f"{SGR.BLOOD}└{'─' * w}┘{SGR.R}"
30
+
31
+ def _box_mid(s, w):
32
+ v = _vis(s)
33
+ return f"{SGR.BLOOD}│{SGR.R} {s}{' ' * (w - v - 1)}{SGR.BLOOD}│{SGR.R}"
34
+
35
+ class TUI:
36
+ def __init__(self, scans, target, workers, sid, mode):
37
+ self.scans = scans
38
+ self.target = target
39
+ self.workers = workers
40
+ self.sid = sid
41
+ self.mode = mode
42
+ self._prev = ""
43
+ self._hits = []
44
+ self._cap = 7
45
+
46
+ def collect(self, h):
47
+ self._hits.append(h)
48
+ if len(self._hits) > 200:
49
+ self._hits = self._hits[-200:]
50
+
51
+ def attach(self):
52
+ for s in self.scans:
53
+ s.on_hit = self.collect
54
+
55
+ def _w(self):
56
+ try:
57
+ return max(64, min(shutil.get_terminal_size((80, 24)).columns - 2, 84))
58
+ except OSError:
59
+ return 78
60
+
61
+ def _header(self, W):
62
+ o = []
63
+ r = SGR.R
64
+ cap = f"{SGR.CRIM}{SGR.B}{CREW}{r} {SGR.IRON}·{r} {SGR.GOLD}{TOOL} 3.0.0{r} {SGR.IRON}·{r} {SGR.SLATE}{CODENAME}{r}"
65
+ left = f" {cap} "
66
+ right = f" {SGR.IRON}{TAGLINE}{r} "
67
+ pad = W - _vis(left) - _vis(right) - 2
68
+ o.append(f"{SGR.BLOOD}┌{'─' * (W - 2)}┐{r}")
69
+ o.append(f"{SGR.BLOOD}│{r}{left}{' ' * pad}{right}{SGR.BLOOD}│{r}")
70
+ for ln in BANNER:
71
+ p = (W - 2 - len(ln)) // 2
72
+ o.append(
73
+ f"{SGR.BLOOD}│{r}{' ' * max(0, p)}{SGR.CRIM}{SGR.B}{ln}{r}"
74
+ f"{' ' * max(0, W - 2 - p - len(ln))}{SGR.BLOOD}│{r}"
75
+ )
76
+ sub = f"{CREW} // {SUFFIX}"
77
+ p = (W - 2 - len(sub)) // 2
78
+ o.append(
79
+ f"{SGR.BLOOD}│{r}{' ' * p}{SGR.SLATE}{sub}{r}"
80
+ f"{' ' * (W - 2 - p - len(sub))}{SGR.BLOOD}│{r}"
81
+ )
82
+ o.append(_box_bot(W - 2))
83
+ return o
84
+
85
+ def _strip(self, W):
86
+ r = SGR.R
87
+ o = []
88
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
89
+ up = fmt_uptime(self.scans[0].meter.elapsed) if self.scans else "00:00:00"
90
+ row1 = (
91
+ f"{SGR.IRON}session{r} {SGR.GOLD}{SGR.B}{self.sid}{r}"
92
+ f" {SGR.IRON}uptime{r} {SGR.IVORY}{up}{r}"
93
+ f" {SGR.IRON}utc{r} {SGR.IVORY}{now}{r}"
94
+ )
95
+ row2 = (
96
+ f"{SGR.IRON}target{r} {SGR.IVORY}{self.target}{r}"
97
+ f" {SGR.IRON}pool{r} {SGR.IVORY}{self.workers}{r}"
98
+ f" {SGR.IRON}mode{r} {SGR.AMBER}{self.mode}{r}"
99
+ )
100
+ o.append(f" {row1}")
101
+ o.append(f" {row2}")
102
+ o.append("")
103
+ return o
104
+
105
+ def _panel(self, s, W):
106
+ r = SGR.R
107
+ o = []
108
+ m = s.meter
109
+ kind = s.kind.upper()
110
+ head = f"── {kind} "
111
+ rule = W - _vis(head) - 4
112
+ o.append(f" {SGR.CRIM}{SGR.B}{head}{r}{SGR.BLOOD}{'─' * max(0, rule)}{r}")
113
+ bar_w = W - 32
114
+ frac = m.progress
115
+ fill = int(frac * bar_w)
116
+ bar = f"{SGR.ICE}{'█' * fill}{SGR.IRON}{'░' * (bar_w - fill)}{r}"
117
+ p95 = m.percentile(0.95)
118
+ p95s = f"{p95:>5.0f}ms" if p95 is not None else " --"
119
+ eta = fmt_dur(m.eta())
120
+ o.append(
121
+ f" {SGR.IRON}live{r} {SGR.GREEN}{SGR.B}{m.ok:>6}{r}"
122
+ f" {SGR.IRON}dead{r} {SGR.CRIM}{m.fail:>6}{r}"
123
+ f" {SGR.IRON}rate{r} {SGR.IVORY}{m.rate():>6.0f}/s{r}"
124
+ f" {SGR.IRON}eta{r} {SGR.IVORY}{eta}{r}"
125
+ f" {SGR.IRON}p95{r} {SGR.ASH}{p95s}{r}"
126
+ )
127
+ o.append(f" {bar} {SGR.ASH}{frac * 100:>5.1f}%{r}")
128
+ o.append(f" {SGR.ICE}{m.spark(bar_w)}{r}")
129
+ o.append("")
130
+ return o
131
+
132
+ def _summary(self, W):
133
+ r = SGR.R
134
+ o = []
135
+ ok_t = sum(s.meter.ok for s in self.scans)
136
+ done_t = sum(s.meter.done for s in self.scans)
137
+ all_t = sum(s.meter.total for s in self.scans)
138
+ pct = (ok_t / done_t * 100.0) if done_t else 0.0
139
+ o.append(f" {SGR.BLOOD}{'─' * (W - 4)}{r}")
140
+ o.append(
141
+ f" {SGR.IRON}alive{r} {SGR.VERD}{SGR.B}{fmt_int(ok_t)}{r}"
142
+ f" {SGR.IRON}checked{r} {SGR.ASH}{fmt_int(done_t)}/{fmt_int(all_t)}{r}"
143
+ f" {SGR.IRON}hit-rate{r} {SGR.GOLD}{pct:.2f}%{r}"
144
+ )
145
+ o.append("")
146
+ return o
147
+
148
+ def _live(self, W):
149
+ r = SGR.R
150
+ o = []
151
+ if not self._hits:
152
+ return o
153
+ head = f"── live hits "
154
+ rule = W - _vis(head) - 4
155
+ o.append(f" {SGR.CRIM}{SGR.B}{head}{r}{SGR.BLOOD}{'─' * max(0, rule)}{r}")
156
+ for h in self._hits[-self._cap:]:
157
+ if h.latency_ms < 200:
158
+ c = SGR.GREEN
159
+ elif h.latency_ms < 800:
160
+ c = SGR.AMBER
161
+ else:
162
+ c = SGR.CRIM
163
+ o.append(
164
+ f" {SGR.CRIM}▸{r} {SGR.ICE}{h.kind:<7}{r}"
165
+ f"{SGR.IVORY}{h.endpoint:<28}{r}"
166
+ f"{c}{h.latency_ms:>6.0f} ms{r}"
167
+ )
168
+ o.append("")
169
+ return o
170
+
171
+ def _footer(self, W):
172
+ r = SGR.R
173
+ o = []
174
+ sig = f"{CREW} // {SUFFIX}"
175
+ p = (W - _vis(sig)) // 2
176
+ o.append(f"{' ' * max(0, p)}{SGR.BLOOD}{SGR.I}{sig}{r}")
177
+ return o
178
+
179
+ def _render(self):
180
+ W = self._w()
181
+ o = []
182
+ o += self._header(W)
183
+ o.append("")
184
+ o += self._strip(W)
185
+ for s in self.scans:
186
+ o += self._panel(s, W)
187
+ o += self._summary(W)
188
+ o += self._live(W)
189
+ o += self._footer(W)
190
+ return "\n".join(o)
191
+
192
+ def draw(self):
193
+ f = self._render()
194
+ if f == self._prev:
195
+ return
196
+ self._prev = f
197
+ sys.stdout.write("\x1b[H\x1b[2J" + f)
198
+ sys.stdout.flush()
199
+
200
+ def enter(self):
201
+ sys.stdout.write("\x1b[?1049h\x1b[?25l")
202
+ sys.stdout.flush()
203
+
204
+ def leave(self):
205
+ sys.stdout.write("\x1b[?25h\x1b[?1049l")
206
+ sys.stdout.flush()
@@ -0,0 +1,14 @@
1
+ import csv, json
2
+ from pathlib import Path
3
+ from .brand import CREW
4
+ def emit_txt(p,hits):
5
+ b="\n".join(e for e,_ in hits)
6
+ Path(p).write_text(b+("\n" if b else ""),encoding="utf-8")
7
+ def emit_json(p,kind,hits):
8
+ payload={"signed_by":CREW,"protocol":kind,"count":len(hits),
9
+ "proxies":[{"endpoint":e,"latency_ms":round(l,2)} for e,l in hits]}
10
+ Path(p).write_text(json.dumps(payload,indent=2),encoding="utf-8")
11
+ def emit_csv(p,hits):
12
+ with Path(p).open("w",newline="",encoding="utf-8") as f:
13
+ w=csv.writer(f); w.writerow(["endpoint","latency_ms"])
14
+ for e,l in hits: w.writerow([e,f"{l:.2f}"])
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: vzrecon
3
+ Version: 3.0.0
4
+ Summary: Protocol-level validator for HTTP/SOCKS4/SOCKS5 proxies
5
+ Author: VZLOI GROUP
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/vzloi/vzrecon
8
+ Project-URL: Source, https://github.com/vzloi/vzrecon
9
+ Project-URL: Issues, https://github.com/vzloi/vzrecon/issues
10
+ Project-URL: Changelog, https://github.com/vzloi/vzrecon/blob/main/CHANGELOG.md
11
+ Keywords: proxy,socks,socks5,socks4,validator,recon,async
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet
22
+ Classifier: Topic :: System :: Networking
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # vzrecon
33
+
34
+ Protocol-level validator for HTTP, SOCKS4 and SOCKS5 proxies.
35
+ Zero dependencies. Termux-native.
36
+
37
+ Signed: VZLOI GROUP // ops
38
+
39
+ ## Install
40
+
41
+ pip install -e .
42
+ vzrecon --self-test
43
+
44
+ ## Use
45
+
46
+ vzrecon # scan cwd for *_proxies.txt
47
+ vzrecon -d ./lists -o ./out # explicit paths
48
+ vzrecon --preset stealth # 200 workers
49
+ vzrecon --preset turbo # 2400 workers
50
+ vzrecon --only http,socks5
51
+ vzrecon --plain
52
+
53
+ ## Protocols
54
+
55
+ - HTTP RFC 7231 CONNECT + forward GET fallback
56
+ - SOCKS5 RFC 1928, no-auth greeting
57
+ - SOCKS4 original spec, IPv4 targets only
58
+
59
+ ## Output
60
+
61
+ - `<proto>_alive.txt` — endpoints, one per line
62
+ - `<proto>_alive.json` — signed_by, protocol, count, proxies[]
63
+ - `<proto>_alive.csv` — endpoint, latency_ms
64
+
65
+ ## Exit codes
66
+
67
+ - 0 scan completed
68
+ - 1 self-test failed
69
+ - 2 config error
70
+ - 3 no source files found
71
+
72
+ ## License
73
+
74
+ MIT. Copyright (c) 2026 VZLOI GROUP.
@@ -0,0 +1,25 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/test_config.py
5
+ tests/test_metrics.py
6
+ tests/test_protocols.py
7
+ vzrecon/__init__.py
8
+ vzrecon/__main__.py
9
+ vzrecon/brand.py
10
+ vzrecon/cli.py
11
+ vzrecon/config.py
12
+ vzrecon/engine.py
13
+ vzrecon/metrics.py
14
+ vzrecon/platform.py
15
+ vzrecon/protocols.py
16
+ vzrecon/py.typed
17
+ vzrecon/selftest.py
18
+ vzrecon/tui.py
19
+ vzrecon/writers.py
20
+ vzrecon.egg-info/PKG-INFO
21
+ vzrecon.egg-info/SOURCES.txt
22
+ vzrecon.egg-info/dependency_links.txt
23
+ vzrecon.egg-info/entry_points.txt
24
+ vzrecon.egg-info/requires.txt
25
+ vzrecon.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vzrecon = vzrecon.cli:main
@@ -0,0 +1,4 @@
1
+
2
+ [dev]
3
+ pytest>=8.0
4
+ pytest-asyncio>=0.23
@@ -0,0 +1 @@
1
+ vzrecon