weed-cli 1.1.4__tar.gz → 1.2.2__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.
- {weed_cli-1.1.4 → weed_cli-1.2.2}/PKG-INFO +1 -1
- {weed_cli-1.1.4 → weed_cli-1.2.2}/node.py +48 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/pyproject.toml +1 -1
- {weed_cli-1.1.4 → weed_cli-1.2.2}/web_ui.py +75 -10
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed.py +9 -1
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed_cli.egg-info/PKG-INFO +1 -1
- {weed_cli-1.1.4 → weed_cli-1.2.2}/LICENSE +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/README.md +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/dht.py +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/discovery_relay.py +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/lightning_settle.py +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/poc_reputation.py +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/setup.cfg +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/shell.py +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/tunnel_relay.py +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.1.4 → weed_cli-1.2.2}/weed_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -541,6 +541,54 @@ def download(host_addr, out_path, tunnel=None, content_hash=None, on_progress=No
|
|
|
541
541
|
return out_path
|
|
542
542
|
|
|
543
543
|
|
|
544
|
+
def verify_local_download(content_hash, relay_urls, path):
|
|
545
|
+
"""Re-checks an already-downloaded file against its own Merkle-
|
|
546
|
+
committed chunk hashes without re-downloading a single byte: one
|
|
547
|
+
INFO+LEAVES round-trip to a currently-advertising host (to learn
|
|
548
|
+
chunk_size and get the leaf hash list — this repo's protocol has no
|
|
549
|
+
way to fetch just that from the file itself), then a purely local
|
|
550
|
+
read-and-rehash of every chunk already on disk. Same trust chain
|
|
551
|
+
download() uses (Merkle-root the leaves against the host's advertised
|
|
552
|
+
sha256 before trusting anything it says), just pointed at bytes that
|
|
553
|
+
are already local instead of a live FETCH stream."""
|
|
554
|
+
from ott import merkle_root
|
|
555
|
+
|
|
556
|
+
hosts = discover_hosts_for(relay_urls, content_hash)
|
|
557
|
+
if not hosts:
|
|
558
|
+
return {'ok': False, 'error': 'no host currently advertising this content_hash on the given relay(s)'}
|
|
559
|
+
match = hosts[0]
|
|
560
|
+
tunnel = _parse_tunnel(match['tunnel']) if match.get('tunnel') else None
|
|
561
|
+
|
|
562
|
+
with open_connection(match['host'], tunnel=tunnel, content_hash=content_hash) as conn:
|
|
563
|
+
if content_hash and not tunnel:
|
|
564
|
+
sel = conn.request(f'SELECT {content_hash}')
|
|
565
|
+
if sel != 'OK':
|
|
566
|
+
return {'ok': False, 'error': f"host rejected SELECT: {sel}"}
|
|
567
|
+
info = json.loads(conn.request('INFO'))
|
|
568
|
+
leaves = json.loads(conn.request('LEAVES'))
|
|
569
|
+
|
|
570
|
+
if len(leaves) != info['n_chunks'] or merkle_root(leaves) != info['sha256'] \
|
|
571
|
+
or info['sha256'] != content_hash:
|
|
572
|
+
return {'ok': False, 'error': "host's advertised LEAVES don't Merkle-root to this "
|
|
573
|
+
"content_hash — refusing to trust it as a reference"}
|
|
574
|
+
if not os.path.isfile(path):
|
|
575
|
+
return {'ok': False, 'error': f'{path} no longer exists on disk'}
|
|
576
|
+
|
|
577
|
+
actual_size = os.path.getsize(path)
|
|
578
|
+
if actual_size != info['size']:
|
|
579
|
+
return {'ok': False, 'error': f'local file is {actual_size:,} bytes, host advertises '
|
|
580
|
+
f'{info["size"]:,} — refetch with re-download'}
|
|
581
|
+
|
|
582
|
+
mismatches = []
|
|
583
|
+
with open(path, 'rb') as f:
|
|
584
|
+
for idx in range(info['n_chunks']):
|
|
585
|
+
data = f.read(info['chunk_size'])
|
|
586
|
+
if hashlib.sha256(data).hexdigest() != leaves[idx]:
|
|
587
|
+
mismatches.append(idx)
|
|
588
|
+
|
|
589
|
+
return {'ok': not mismatches, 'n_chunks': info['n_chunks'], 'mismatches': mismatches}
|
|
590
|
+
|
|
591
|
+
|
|
544
592
|
# ── possession challenge + price auction, wired to real discovery ───────
|
|
545
593
|
#
|
|
546
594
|
# Everything below stitches poc_challenge_auction.py (challenge-gate a
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weed-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.2"
|
|
8
8
|
description = "Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -38,6 +38,14 @@ WEB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web')
|
|
|
38
38
|
DEFAULT_RELAY = 'http://127.0.0.1:9101'
|
|
39
39
|
LIBRARY_PATH = os.path.expanduser('~/.weed_library.json')
|
|
40
40
|
|
|
41
|
+
# script-relative, not CWD-relative -- same reasoning as WEB_DIR above, so
|
|
42
|
+
# downloads land in the same place regardless of the directory this was
|
|
43
|
+
# launched from (previously defaulted to a bare filename, which meant
|
|
44
|
+
# download_<hash> files scattered directly into whatever the CWD happened
|
|
45
|
+
# to be when web_ui.py was started)
|
|
46
|
+
DOWNLOADS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'downloads')
|
|
47
|
+
os.makedirs(DOWNLOADS_DIR, exist_ok=True)
|
|
48
|
+
|
|
41
49
|
|
|
42
50
|
class _JobStdout:
|
|
43
51
|
"""Replaces sys.stdout for this whole process so a background job
|
|
@@ -129,7 +137,7 @@ def _rehydrate_jobs_from_library():
|
|
|
129
137
|
'status': 'done', 'idx': 0, 'n_chunks': None,
|
|
130
138
|
'content_hash': content_hash, 'path': rec['path'],
|
|
131
139
|
'title': rec.get('title'), 'size': rec.get('size'), 'bps': rec.get('bps'),
|
|
132
|
-
'error': None,
|
|
140
|
+
'signer_pubkey': rec.get('signer_pubkey'), 'error': None,
|
|
133
141
|
}
|
|
134
142
|
_lan_url = None # set once in run_web_ui() -- the base URL a phone on the
|
|
135
143
|
# same LAN can actually reach this server at, or None if
|
|
@@ -203,7 +211,8 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
203
211
|
_hosts[host_id].update(status='error', error=f'{type(e).__name__}: {e}')
|
|
204
212
|
|
|
205
213
|
|
|
206
|
-
def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightning, title=None
|
|
214
|
+
def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightning, title=None,
|
|
215
|
+
signer_pubkey=None):
|
|
207
216
|
def on_progress(idx, n_chunks):
|
|
208
217
|
with _lock:
|
|
209
218
|
_jobs[job_id].update(idx=idx, n_chunks=n_chunks)
|
|
@@ -221,6 +230,7 @@ def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightni
|
|
|
221
230
|
_library['downloads'][content_hash] = {
|
|
222
231
|
'content_hash': content_hash, 'job_id': job_id, 'path': path,
|
|
223
232
|
'title': title, 'downloaded_at': time.time(), 'size': size, 'bps': bps,
|
|
233
|
+
'signer_pubkey': signer_pubkey,
|
|
224
234
|
}
|
|
225
235
|
_save_library()
|
|
226
236
|
except SystemExit as e:
|
|
@@ -231,6 +241,19 @@ def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightni
|
|
|
231
241
|
_jobs[job_id].update(status='error', error=f'{type(e).__name__}: {e}')
|
|
232
242
|
|
|
233
243
|
|
|
244
|
+
class WebUIServer(ThreadingHTTPServer):
|
|
245
|
+
def handle_error(self, request, client_address):
|
|
246
|
+
"""A client opening a TCP connection and resetting it before ever
|
|
247
|
+
sending a request line is routine, not a bug -- phone browsers,
|
|
248
|
+
a QR-scanner app's in-app preview, and Chrome's own speculative
|
|
249
|
+
preconnects all do this. socketserver's default handle_error
|
|
250
|
+
prints a full traceback for every one of these; only genuinely
|
|
251
|
+
unexpected errors get that treatment here."""
|
|
252
|
+
if sys.exc_info()[0] in (ConnectionResetError, BrokenPipeError, TimeoutError):
|
|
253
|
+
return
|
|
254
|
+
super().handle_error(request, client_address)
|
|
255
|
+
|
|
256
|
+
|
|
234
257
|
class Handler(BaseHTTPRequestHandler):
|
|
235
258
|
def log_message(self, fmt, *args):
|
|
236
259
|
pass # quiet — this is a local UI, not a service worth logging every hit for
|
|
@@ -296,6 +319,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
296
319
|
handlers = {
|
|
297
320
|
'/api/host': self._handle_host, '/api/download': self._handle_download,
|
|
298
321
|
'/api/like': self._handle_like, '/api/subscribe': self._handle_subscribe,
|
|
322
|
+
'/api/verify': self._handle_verify,
|
|
299
323
|
}
|
|
300
324
|
handler = handlers.get(path)
|
|
301
325
|
if not handler:
|
|
@@ -330,17 +354,20 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
330
354
|
if not content_hash:
|
|
331
355
|
return self._json({'error': 'content_hash required'}, status=400)
|
|
332
356
|
relay_urls = _as_list(body.get('relay'), [DEFAULT_RELAY])
|
|
333
|
-
out_path = body.get('out_path') or f'download_{content_hash[:16]}'
|
|
357
|
+
out_path = body.get('out_path') or os.path.join(DOWNLOADS_DIR, f'download_{content_hash[:16]}')
|
|
334
358
|
k = int(body.get('k') or 3)
|
|
335
359
|
use_lightning = bool(body.get('lightning'))
|
|
336
360
|
title = body.get('title')
|
|
361
|
+
signer_pubkey = body.get('signer_pubkey')
|
|
337
362
|
|
|
338
363
|
job_id = uuid.uuid4().hex[:12]
|
|
339
364
|
with _lock:
|
|
340
365
|
_jobs[job_id] = {'status': 'running', 'idx': 0, 'n_chunks': None,
|
|
341
|
-
'content_hash': content_hash, 'path': None, 'title': title,
|
|
366
|
+
'content_hash': content_hash, 'path': None, 'title': title,
|
|
367
|
+
'signer_pubkey': signer_pubkey, 'error': None}
|
|
342
368
|
threading.Thread(target=_run_download_job,
|
|
343
|
-
args=(job_id, content_hash, relay_urls, out_path, k, use_lightning, title
|
|
369
|
+
args=(job_id, content_hash, relay_urls, out_path, k, use_lightning, title,
|
|
370
|
+
signer_pubkey),
|
|
344
371
|
daemon=True).start()
|
|
345
372
|
self._json({'job_id': job_id})
|
|
346
373
|
|
|
@@ -370,6 +397,26 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
370
397
|
_save_library()
|
|
371
398
|
self._json({'result': result})
|
|
372
399
|
|
|
400
|
+
def _handle_verify(self, body):
|
|
401
|
+
"""Re-checks an already-downloaded file against its own
|
|
402
|
+
Merkle-committed chunk hashes without re-fetching it -- see
|
|
403
|
+
node.verify_local_download's docstring. Runs synchronously (this
|
|
404
|
+
is a local read + one INFO/LEAVES round-trip, not a real
|
|
405
|
+
download, so it's fast enough not to need a job/poll dance like
|
|
406
|
+
/api/download does)."""
|
|
407
|
+
content_hash = body.get('content_hash')
|
|
408
|
+
if not content_hash:
|
|
409
|
+
return self._json({'error': 'content_hash required'}, status=400)
|
|
410
|
+
with _lock:
|
|
411
|
+
rec = _library['downloads'].get(content_hash)
|
|
412
|
+
if not rec:
|
|
413
|
+
return self._json({'error': 'no local download on record for this content_hash'},
|
|
414
|
+
status=404)
|
|
415
|
+
relay_urls = _as_list(body.get('relay'), [DEFAULT_RELAY])
|
|
416
|
+
with _quiet():
|
|
417
|
+
result = node.verify_local_download(content_hash, relay_urls, rec['path'])
|
|
418
|
+
self._json(result)
|
|
419
|
+
|
|
373
420
|
def _handle_stream(self, job_id):
|
|
374
421
|
"""Serve an already-downloaded job's file with real HTTP range
|
|
375
422
|
support, so a <video> tag can seek/scrub instead of just
|
|
@@ -468,17 +515,29 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
468
515
|
self.send_response(200)
|
|
469
516
|
self.send_header('Content-Type', ctype or 'application/octet-stream')
|
|
470
517
|
self.send_header('Content-Length', str(len(body)))
|
|
518
|
+
# this UI's still under active iteration -- every static file is
|
|
519
|
+
# re-read from disk fresh on every request (no server-side
|
|
520
|
+
# caching at all), so the only thing that can ever be stale is a
|
|
521
|
+
# browser hanging onto an old copy of index.html/vue-app.js from
|
|
522
|
+
# its own heuristics. No new dependency needed to fix that, just
|
|
523
|
+
# telling it not to.
|
|
524
|
+
self.send_header('Cache-Control', 'no-store')
|
|
471
525
|
self.end_headers()
|
|
472
526
|
self.wfile.write(body)
|
|
473
527
|
|
|
474
528
|
|
|
475
|
-
def run_web_ui(port=8080, bind_host='127.0.0.1', quiet=False):
|
|
529
|
+
def run_web_ui(port=8080, bind_host='127.0.0.1', quiet=False, advertise_host=None):
|
|
476
530
|
global _lan_url
|
|
477
531
|
_load_library()
|
|
478
532
|
_rehydrate_jobs_from_library()
|
|
479
|
-
srv =
|
|
480
|
-
|
|
481
|
-
|
|
533
|
+
srv = WebUIServer((bind_host, port), Handler)
|
|
534
|
+
|
|
535
|
+
# advertise_host is an escape hatch for _detect_lan_ip()'s UDP-route
|
|
536
|
+
# trick guessing wrong (multiple interfaces/VPNs, sandboxed or
|
|
537
|
+
# container networking, no outbound route at all) -- same failure
|
|
538
|
+
# shape as "the QR/lan-url doesn't point at a reachable address",
|
|
539
|
+
# just fixed by telling this explicitly instead of guessing
|
|
540
|
+
reachable_host = advertise_host or (_detect_lan_ip() if bind_host == '0.0.0.0' else bind_host)
|
|
482
541
|
if bind_host != '127.0.0.1' and reachable_host:
|
|
483
542
|
_lan_url = f'http://{reachable_host}:{port}/'
|
|
484
543
|
|
|
@@ -513,9 +572,15 @@ def main():
|
|
|
513
572
|
parser.add_argument('--bind', default='127.0.0.1',
|
|
514
573
|
help='bind address (default: 127.0.0.1, local only -- no auth is '
|
|
515
574
|
'built, so only widen this on a network you trust)')
|
|
575
|
+
parser.add_argument('--advertise-host',
|
|
576
|
+
help="IP/hostname to put in the phone QR and lan-url instead of "
|
|
577
|
+
"auto-detecting it -- use this if the QR at startup was missing "
|
|
578
|
+
"or pointed at the wrong address (auto-detection guesses via an "
|
|
579
|
+
"outbound route, which can pick the wrong interface or fail "
|
|
580
|
+
"outright on unusual networking)")
|
|
516
581
|
args = parser.parse_args()
|
|
517
582
|
port = args.port_flag if args.port_flag is not None else args.port
|
|
518
|
-
run_web_ui(port, bind_host=args.bind)
|
|
583
|
+
run_web_ui(port, bind_host=args.bind, advertise_host=args.advertise_host)
|
|
519
584
|
|
|
520
585
|
|
|
521
586
|
if __name__ == '__main__':
|
|
@@ -118,6 +118,10 @@ def build_parser():
|
|
|
118
118
|
p_web.add_argument('--bind', default='127.0.0.1',
|
|
119
119
|
help='bind address (default 127.0.0.1 — local only; no auth is built, '
|
|
120
120
|
'so only widen this on a network you trust)')
|
|
121
|
+
p_web.add_argument('--advertise-host',
|
|
122
|
+
help='IP/hostname for the phone QR and lan-url instead of '
|
|
123
|
+
'auto-detecting it — use this if the startup QR was missing or '
|
|
124
|
+
'pointed at the wrong address')
|
|
121
125
|
|
|
122
126
|
p_serve = sub.add_parser('serve', help='alias for "web" with positional args, '
|
|
123
127
|
'e.g. `serve 0.0.0.0 8080`')
|
|
@@ -126,6 +130,10 @@ def build_parser():
|
|
|
126
130
|
'from your phone — no auth is built, only widen this on a '
|
|
127
131
|
'network you trust)')
|
|
128
132
|
p_serve.add_argument('port', nargs='?', type=int, default=8080)
|
|
133
|
+
p_serve.add_argument('--advertise-host',
|
|
134
|
+
help='IP/hostname for the phone QR and lan-url instead of '
|
|
135
|
+
'auto-detecting it — use this if the startup QR was missing or '
|
|
136
|
+
'pointed at the wrong address')
|
|
129
137
|
|
|
130
138
|
return parser
|
|
131
139
|
|
|
@@ -209,7 +217,7 @@ def cmd_subscribe(args):
|
|
|
209
217
|
|
|
210
218
|
def cmd_web(args):
|
|
211
219
|
import web_ui
|
|
212
|
-
web_ui.run_web_ui(port=args.port, bind_host=args.bind)
|
|
220
|
+
web_ui.run_web_ui(port=args.port, bind_host=args.bind, advertise_host=args.advertise_host)
|
|
213
221
|
|
|
214
222
|
|
|
215
223
|
NATIVE_COMMANDS = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|