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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weed-cli
3
- Version: 1.1.4
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.1.4"
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, 'error': None}
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 = ThreadingHTTPServer((bind_host, port), Handler)
480
-
481
- reachable_host = _detect_lan_ip() if bind_host == '0.0.0.0' else bind_host
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.1.4
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