|
20 | 20 |
|
21 | 21 | import os |
22 | 22 | import re |
| 23 | +import io |
23 | 24 | import sys |
24 | 25 | import time |
25 | 26 | import socket |
@@ -2748,359 +2749,6 @@ def send_from_fileobj(fileobj, host, port=3124, proto="tcp", timeout=None, |
2748 | 2749 | return total |
2749 | 2750 |
|
2750 | 2751 |
|
2751 | | -def recv_to_fileobj(fileobj, host="", port=3124, proto="tcp", timeout=None, |
2752 | | - max_bytes=None, chunk_size=65536, backlog=1, |
2753 | | - use_ssl=False, ssl_verify=True, ssl_ca_file=None, |
2754 | | - ssl_certfile=None, ssl_keyfile=None, |
2755 | | - require_auth=False, expected_user=None, expected_pass=None, |
2756 | | - total_timeout=None, expect_scope=None, |
2757 | | - on_progress=None, rate_limit_bps=None, |
2758 | | - enforce_path=True, wait_seconds=None): |
2759 | | - """ |
2760 | | - Receive bytes into fileobj over TCP/UDP. |
2761 | | -
|
2762 | | - Path enforcement: |
2763 | | - - UDP: expects 'PATH <...>\\n' control frame first (if enforce_path). |
2764 | | - - TCP: reads first line 'PATH <...>\\n' before auth/payload (if enforce_path). |
2765 | | -
|
2766 | | - UDP control frames understood: PATH, LEN, HASH, DONE (+ AF1 auth blob). |
2767 | | -
|
2768 | | - wait_seconds (TCP only): overall accept window to wait for a client |
2769 | | - (mirrors the HTTP server behavior). None = previous behavior (single accept |
2770 | | - with 'timeout' as the accept timeout). |
2771 | | - """ |
2772 | | - proto = (proto or "tcp").lower() |
2773 | | - port = int(port) |
2774 | | - total = 0 |
2775 | | - |
2776 | | - start_ts = time.time() |
2777 | | - def _time_left(): |
2778 | | - if total_timeout is None: |
2779 | | - return None |
2780 | | - left = total_timeout - (time.time() - start_ts) |
2781 | | - return 0.0 if left <= 0 else left |
2782 | | - |
2783 | | - def _set_effective_timeout(socklike, base_timeout): |
2784 | | - left = _time_left() |
2785 | | - if left == 0.0: |
2786 | | - return False |
2787 | | - eff = base_timeout |
2788 | | - if left is not None: |
2789 | | - eff = left if eff is None else min(eff, left) |
2790 | | - if eff is not None: |
2791 | | - try: |
2792 | | - socklike.settimeout(eff) |
2793 | | - except Exception: |
2794 | | - pass |
2795 | | - return True |
2796 | | - |
2797 | | - if proto not in ("tcp", "udp"): |
2798 | | - raise ValueError("proto must be 'tcp' or 'udp'") |
2799 | | - |
2800 | | - # ---------------- UDP server ---------------- |
2801 | | - if proto == "udp": |
2802 | | - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
2803 | | - authed_addr = None |
2804 | | - expected_len = None |
2805 | | - expected_sha = None |
2806 | | - path_checked = (not enforce_path) |
2807 | | - |
2808 | | - try: |
2809 | | - sock.bind(("", port)) |
2810 | | - if timeout is None: |
2811 | | - try: sock.settimeout(10.0) |
2812 | | - except Exception: pass |
2813 | | - |
2814 | | - recvd_so_far = 0 |
2815 | | - last_cb_ts = monotonic() |
2816 | | - rl_ts = last_cb_ts |
2817 | | - rl_bytes = 0 |
2818 | | - |
2819 | | - while True: |
2820 | | - if _time_left() == 0.0: |
2821 | | - if expected_len is not None and total < expected_len: |
2822 | | - raise RuntimeError("UDP receive aborted by total_timeout before full payload received") |
2823 | | - break |
2824 | | - if (max_bytes is not None) and (total >= max_bytes): |
2825 | | - break |
2826 | | - |
2827 | | - if not _set_effective_timeout(sock, timeout): |
2828 | | - if expected_len is not None and total < expected_len: |
2829 | | - raise RuntimeError("UDP receive timed out before full payload received") |
2830 | | - if expected_len is None and total > 0: |
2831 | | - raise RuntimeError("UDP receive timed out with unknown length; partial data") |
2832 | | - if expected_len is None and total == 0: |
2833 | | - raise RuntimeError("UDP receive: no packets received before timeout (is the sender running?)") |
2834 | | - break |
2835 | | - |
2836 | | - try: |
2837 | | - data, addr = sock.recvfrom(chunk_size) |
2838 | | - except socket.timeout: |
2839 | | - if expected_len is not None and total < expected_len: |
2840 | | - raise RuntimeError("UDP receive idle-timeout before full payload received") |
2841 | | - if expected_len is None and total > 0: |
2842 | | - raise RuntimeError("UDP receive idle-timeout with unknown length; partial data") |
2843 | | - if expected_len is None and total == 0: |
2844 | | - raise RuntimeError("UDP receive: no packets received before timeout (is the sender running?)") |
2845 | | - break |
2846 | | - |
2847 | | - if not data: |
2848 | | - continue |
2849 | | - |
2850 | | - # (0) PATH first (strict) |
2851 | | - if not path_checked and data.startswith(b"PATH "): |
2852 | | - got_path = _unquote_path_from_wire(data[5:].strip()) |
2853 | | - if _to_text(got_path) != _to_text(expect_scope or u""): |
2854 | | - raise RuntimeError("UDP path mismatch: got %r expected %r" |
2855 | | - % (got_path, expect_scope)) |
2856 | | - path_checked = True |
2857 | | - continue |
2858 | | - if enforce_path and not path_checked: |
2859 | | - if not data.startswith(b"PATH "): |
2860 | | - continue # ignore until PATH arrives |
2861 | | - |
2862 | | - # (0b) Control frames |
2863 | | - if data.startswith(b"LEN ") and expected_len is None: |
2864 | | - try: |
2865 | | - parts = data.strip().split() |
2866 | | - n = int(parts[1]) |
2867 | | - expected_len = (None if n < 0 else n) |
2868 | | - if len(parts) >= 3: |
2869 | | - expected_sha = parts[2].decode("ascii") |
2870 | | - except Exception: |
2871 | | - expected_len = None; expected_sha = None |
2872 | | - continue |
2873 | | - |
2874 | | - if data.startswith(b"HASH "): |
2875 | | - try: |
2876 | | - expected_sha = data.strip().split()[1].decode("ascii") |
2877 | | - except Exception: |
2878 | | - expected_sha = None |
2879 | | - continue |
2880 | | - |
2881 | | - if data == b"DONE\n": |
2882 | | - # Treat DONE as end-of-transfer. If we know the expected length, |
2883 | | - # ignore early DONE until we have all bytes (reduces truncation risk). |
2884 | | - if expected_len is None or total_received >= expected_len: |
2885 | | - break |
2886 | | - else: |
2887 | | - continue |
2888 | | - # (1) Auth (if required) |
2889 | | - if authed_addr is None and require_auth: |
2890 | | - ok = False |
2891 | | - v_ok, v_user, v_scope, _r, v_len, v_sha = verify_auth_blob_v1( |
2892 | | - data, expected_user=expected_user, secret=expected_pass, |
2893 | | - max_skew=600, expect_scope=expect_scope |
2894 | | - ) |
2895 | | - if v_ok: |
2896 | | - ok = True |
2897 | | - if expected_len is None: |
2898 | | - expected_len = v_len |
2899 | | - if expected_sha is None: |
2900 | | - expected_sha = v_sha |
2901 | | - else: |
2902 | | - user, pw = _parse_auth_blob_legacy(data) |
2903 | | - ok = (user is not None and |
2904 | | - (expected_user is None or user == _to_bytes(expected_user)) and |
2905 | | - (expected_pass is None or pw == _to_bytes(expected_pass))) |
2906 | | - try: |
2907 | | - sock.sendto((_OK if ok else _NO), addr) |
2908 | | - except Exception: |
2909 | | - pass |
2910 | | - if ok: |
2911 | | - authed_addr = addr |
2912 | | - continue |
2913 | | - |
2914 | | - if require_auth and addr != authed_addr: |
2915 | | - continue |
2916 | | - |
2917 | | - # (2) Payload |
2918 | | - fileobj.write(data) |
2919 | | - try: fileobj.flush() |
2920 | | - except Exception: pass |
2921 | | - total += len(data) |
2922 | | - recvd_so_far += len(data) |
2923 | | - |
2924 | | - if rate_limit_bps: |
2925 | | - sleep_s, rl_ts, rl_bytes = _pace_rate(rl_ts, rl_bytes, rate_limit_bps, len(data)) |
2926 | | - if sleep_s > 0.0: |
2927 | | - time.sleep(min(sleep_s, 0.25)) |
2928 | | - |
2929 | | - if on_progress and (monotonic() - last_cb_ts) >= 0.1: |
2930 | | - try: on_progress(recvd_so_far, expected_len) |
2931 | | - except Exception: pass |
2932 | | - last_cb_ts = monotonic() |
2933 | | - |
2934 | | - if expected_len is not None and total >= expected_len: |
2935 | | - break |
2936 | | - |
2937 | | - # Post-conditions |
2938 | | - if expected_len is not None and total != expected_len: |
2939 | | - raise RuntimeError("UDP receive incomplete: got %d of %s bytes" % (total, expected_len)) |
2940 | | - |
2941 | | - if expected_sha: |
2942 | | - import hashlib |
2943 | | - try: |
2944 | | - cur = fileobj.tell(); fileobj.seek(0) |
2945 | | - except Exception: |
2946 | | - cur = None |
2947 | | - h = hashlib.sha256(); _HSZ = 1024 * 1024 |
2948 | | - while True: |
2949 | | - blk = fileobj.read(_HSZ) |
2950 | | - if not blk: break |
2951 | | - h.update(_to_bytes(blk)) |
2952 | | - got = h.hexdigest() |
2953 | | - if cur is not None: |
2954 | | - try: fileobj.seek(cur) |
2955 | | - except Exception: pass |
2956 | | - if got != expected_sha: |
2957 | | - raise RuntimeError("UDP checksum mismatch: got %s expected %s" % (got, expected_sha)) |
2958 | | - |
2959 | | - finally: |
2960 | | - try: sock.close() |
2961 | | - except Exception: pass |
2962 | | - return total |
2963 | | - |
2964 | | - # ---------------- TCP server (one-shot with optional wait window) ---------------- |
2965 | | - srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
2966 | | - try: |
2967 | | - try: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
2968 | | - except Exception: pass |
2969 | | - srv.bind((host or "", port)) |
2970 | | - srv.listen(int(backlog) if backlog else 1) |
2971 | | - |
2972 | | - bytes_written = 0 |
2973 | | - started = time.time() |
2974 | | - |
2975 | | - # per-accept wait |
2976 | | - per_accept = float(timeout) if timeout is not None else 1.0 |
2977 | | - try: srv.settimeout(per_accept) |
2978 | | - except Exception: pass |
2979 | | - |
2980 | | - while True: |
2981 | | - if bytes_written > 0: |
2982 | | - break |
2983 | | - if wait_seconds is not None and (time.time() - started) >= wait_seconds: |
2984 | | - break |
2985 | | - |
2986 | | - try: |
2987 | | - conn, _peer = srv.accept() |
2988 | | - except socket.timeout: |
2989 | | - continue |
2990 | | - except Exception: |
2991 | | - break |
2992 | | - |
2993 | | - # TLS |
2994 | | - if use_ssl: |
2995 | | - if not _ssl_available(): |
2996 | | - try: conn.close() |
2997 | | - except Exception: pass |
2998 | | - break |
2999 | | - if not ssl_certfile: |
3000 | | - try: conn.close() |
3001 | | - except Exception: pass |
3002 | | - raise ValueError("TLS server requires ssl_certfile (and usually ssl_keyfile).") |
3003 | | - conn = _ssl_wrap_socket(conn, server_side=True, server_hostname=None, |
3004 | | - verify=ssl_verify, ca_file=ssl_ca_file, |
3005 | | - certfile=ssl_certfile, keyfile=ssl_keyfile) |
3006 | | - |
3007 | | - recvd_so_far = 0 |
3008 | | - last_cb_ts = monotonic() |
3009 | | - rl_ts = last_cb_ts |
3010 | | - rl_bytes = 0 |
3011 | | - |
3012 | | - try: |
3013 | | - # (0) PATH line (if enforced) |
3014 | | - if enforce_path: |
3015 | | - line = _recv_line(conn, maxlen=4096, timeout=timeout) |
3016 | | - if not line or not line.startswith(b"PATH "): |
3017 | | - try: conn.close() |
3018 | | - except Exception: pass |
3019 | | - continue |
3020 | | - got_path = _unquote_path_from_wire(line[5:].strip()) |
3021 | | - if _to_text(got_path) != _to_text(expect_scope or u""): |
3022 | | - try: conn.close() |
3023 | | - except Exception: pass |
3024 | | - raise RuntimeError("TCP path mismatch: got %r expected %r" |
3025 | | - % (got_path, expect_scope)) |
3026 | | - |
3027 | | - # (1) Auth preface |
3028 | | - if require_auth: |
3029 | | - if not _set_effective_timeout(conn, timeout): |
3030 | | - try: conn.close() |
3031 | | - except Exception: pass |
3032 | | - continue |
3033 | | - try: |
3034 | | - preface = conn.recv(2048) |
3035 | | - except socket.timeout: |
3036 | | - try: conn.sendall(_NO) |
3037 | | - except Exception: pass |
3038 | | - try: conn.close() |
3039 | | - except Exception: pass |
3040 | | - continue |
3041 | | - |
3042 | | - ok = False |
3043 | | - v_ok, v_user, v_scope, _r, v_len, v_sha = verify_auth_blob_v1( |
3044 | | - preface or b"", expected_user=expected_user, secret=expected_pass, |
3045 | | - max_skew=600, expect_scope=expect_scope |
3046 | | - ) |
3047 | | - if v_ok: |
3048 | | - ok = True |
3049 | | - else: |
3050 | | - user, pw = _parse_auth_blob_legacy(preface or b"") |
3051 | | - ok = (user is not None and |
3052 | | - (expected_user is None or user == _to_bytes(expected_user)) and |
3053 | | - (expected_pass is None or pw == _to_bytes(expected_pass))) |
3054 | | - try: conn.sendall(_OK if ok else _NO) |
3055 | | - except Exception: pass |
3056 | | - if not ok: |
3057 | | - try: conn.close() |
3058 | | - except Exception: pass |
3059 | | - continue |
3060 | | - |
3061 | | - # (2) Payload loop |
3062 | | - while True: |
3063 | | - if _time_left() == 0.0: break |
3064 | | - if (max_bytes is not None) and (bytes_written >= max_bytes): break |
3065 | | - |
3066 | | - if not _set_effective_timeout(conn, timeout): |
3067 | | - break |
3068 | | - try: |
3069 | | - data = conn.recv(chunk_size) |
3070 | | - except socket.timeout: |
3071 | | - break |
3072 | | - if not data: |
3073 | | - break |
3074 | | - |
3075 | | - fileobj.write(data) |
3076 | | - try: fileobj.flush() |
3077 | | - except Exception: pass |
3078 | | - total += len(data) |
3079 | | - bytes_written += len(data) |
3080 | | - recvd_so_far += len(data) |
3081 | | - |
3082 | | - if rate_limit_bps: |
3083 | | - sleep_s, rl_ts, rl_bytes = _pace_rate(rl_ts, rl_bytes, rate_limit_bps, len(data)) |
3084 | | - if sleep_s > 0.0: |
3085 | | - time.sleep(min(sleep_s, 0.25)) |
3086 | | - |
3087 | | - if on_progress and (monotonic() - last_cb_ts) >= 0.1: |
3088 | | - try: on_progress(recvd_so_far, max_bytes) |
3089 | | - except Exception: pass |
3090 | | - last_cb_ts = monotonic() |
3091 | | - |
3092 | | - finally: |
3093 | | - try: conn.shutdown(socket.SHUT_RD) |
3094 | | - except Exception: pass |
3095 | | - try: conn.close() |
3096 | | - except Exception: pass |
3097 | | - |
3098 | | - return total |
3099 | | - |
3100 | | - finally: |
3101 | | - try: srv.close() |
3102 | | - except Exception: pass |
3103 | | - |
3104 | 2752 | def run_tcp_file_server(fileobj, url, on_progress=None): |
3105 | 2753 | """ |
3106 | 2754 | One-shot TCP uploader: wait for a client, authenticate (optional), |
@@ -3941,7 +3589,7 @@ def recv_to_fileobj(fileobj, host="", port=0, proto="tcp", timeout=None, |
3941 | 3589 | require_auth=False, expected_user=None, expected_pass=None, |
3942 | 3590 | total_timeout=None, expect_scope=None, |
3943 | 3591 | on_progress=None, rate_limit_bps=None, |
3944 | | - enforce_path=True, wait_seconds=None): |
| 3592 | + enforce_path=True, wait_seconds=None, **_kwargs): |
3945 | 3593 | """ |
3946 | 3594 | Receive bytes into fileobj over TCP/UDP. |
3947 | 3595 |
|
@@ -4850,7 +4498,7 @@ def recv_via_url(fileobj, url, recv_to_fileobj_func=recv_to_fileobj): |
4850 | 4498 | require_auth=require_auth, |
4851 | 4499 | expected_user=(o["user"] if require_auth else None), |
4852 | 4500 | expected_pass=(o["pw"] if require_auth else None), |
4853 | | - auth_scope=o["path"], |
| 4501 | + expect_scope=o["path"], |
4854 | 4502 | want_sha=o["want_sha"], |
4855 | 4503 | enforce_path=o["enforce_path"], |
4856 | 4504 | expected_path=o["path"], |
|
0 commit comments