11import threading
2- from typing import Any , Callable , Dict , Optional , List
2+ from typing import Any , Callable , Dict , Optional , List , Tuple
33import logging
44import base64
55import os
66import time
77import urllib .parse
8+ import json
9+ import struct
810import requests
911from requests .exceptions import RetryError , HTTPError
1012
1315from bsv .auth .requested_certificate_set import RequestedCertificateSet
1416from bsv .auth .verifiable_certificate import VerifiableCertificate
1517from bsv .auth .transports .simplified_http_transport import SimplifiedHTTPTransport
18+ from bsv .auth .peer import PeerOptions
1619
1720class SimplifiedFetchRequestOptions :
1821 def __init__ (self , method : str = "GET" , headers : Optional [Dict [str , str ]] = None , body : Optional [bytes ] = None , retry_counter : Optional [int ] = None ):
@@ -54,17 +57,29 @@ def fetch(self, ctx: Any, url_str: str, config: Optional[SimplifiedFetchRequestO
5457 # Create peer if needed
5558 if base_url not in self .peers :
5659 transport = SimplifiedHTTPTransport (base_url )
57- peer = Peer (
60+ peer = Peer (PeerOptions (
5861 wallet = self .wallet ,
5962 transport = transport ,
6063 certificates_to_request = self .requested_certificates ,
6164 session_manager = self .session_manager
62- )
65+ ))
6366 auth_peer = AuthPeer ()
6467 auth_peer .peer = peer
6568 self .peers [base_url ] = auth_peer
66- # Set up certificate received/requested listeners(省略: 必要に応じて追加)
69+ # Set up certificate listeners similar to TS/Go implementations
70+ def _on_certs_received (sender_public_key , certs ):
71+ try :
72+ self .certificates_received .extend (certs or [])
73+ except Exception :
74+ pass
75+ self .peers [base_url ].peer .listen_for_certificates_received (_on_certs_received )
6776 peer_to_use = self .peers [base_url ]
77+ # If mutual auth explicitly unsupported for this base URL, fall back to normal HTTP
78+ if peer_to_use .supports_mutual_auth is not None and peer_to_use .supports_mutual_auth is False :
79+ resp = self .handle_fetch_and_validate (url_str , config , peer_to_use )
80+ if getattr (resp , 'status_code' , None ) == 402 :
81+ return self .handle_payment_and_retry (ctx , url_str , config , resp )
82+ return resp
6883 # Generate request nonce
6984 request_nonce = os .urandom (32 )
7085 request_nonce_b64 = base64 .b64encode (request_nonce ).decode ()
@@ -86,32 +101,132 @@ def fetch(self, ctx: Any, url_str: str, config: Optional[SimplifiedFetchRequestO
86101 }
87102 # Peerのgeneral messageリスナー登録
88103 def on_general_message (sender_public_key , payload ):
89- # 先頭32バイトがresponse_nonce
90- if not payload or len (payload ) < 32 :
104+ try :
105+ resp_obj = self ._parse_general_response (sender_public_key , payload , request_nonce_b64 , url_str , config )
106+ except Exception :
91107 return
92- response_nonce = payload [:32 ]
93- response_nonce_b64 = base64 .b64encode (response_nonce ).decode ()
94- if response_nonce_b64 != request_nonce_b64 :
95- return # 自分のリクエストでなければ無視
96- # 以降はHTTPレスポンスのデシリアライズ等(省略: 必要に応じて実装)
97- self .callbacks [request_nonce_b64 ]['resolve' ](payload )
108+ if resp_obj is None :
109+ return
110+ self .callbacks [request_nonce_b64 ]['resolve' ](resp_obj )
98111 listener_id = peer_to_use .peer .listen_for_general_messages (on_general_message )
99112 try :
100113 # Peer経由で送信(ToPeer相当)
101114 err = peer_to_use .peer .to_peer (ctx , request_data , None , 30000 )
102115 if err :
103- self .callbacks [request_nonce_b64 ]['reject' ](err )
116+ # Fallback handling similar to TS/Go
117+ err_str = str (err )
118+ if 'Session not found for nonce' in err_str :
119+ try :
120+ del self .peers [base_url ]
121+ except Exception :
122+ pass
123+ if config .retry_counter is None :
124+ config .retry_counter = 3
125+ # Retry request afresh
126+ self .callbacks [request_nonce_b64 ]['resolve' ](self .fetch (ctx , url_str , config ))
127+ elif 'HTTP server failed to authenticate' in err_str :
128+ try :
129+ resp = self .handle_fetch_and_validate (url_str , config , peer_to_use )
130+ self .callbacks [request_nonce_b64 ]['resolve' ](resp )
131+ except Exception as e :
132+ self .callbacks [request_nonce_b64 ]['reject' ](e )
133+ else :
134+ self .callbacks [request_nonce_b64 ]['reject' ](err )
104135 except Exception as e :
105136 self .callbacks [request_nonce_b64 ]['reject' ](e )
106137 # レスポンス待機(またはタイムアウト)
107138 response_event .wait (timeout = 30 ) # 30秒タイムアウト
108139 # コールバック解除
109140 peer_to_use .peer .stop_listening_for_general_messages (listener_id )
110141 self .callbacks .pop (request_nonce_b64 , None )
111- # 結果返却
142+ # 結果返却
112143 if response_holder ['err' ]:
113144 raise RuntimeError (response_holder ['err' ])
114- return response_holder ['resp' ]
145+ resp_obj = response_holder ['resp' ]
146+ try :
147+ if getattr (resp_obj , 'status_code' , None ) == 402 :
148+ return self .handle_payment_and_retry (ctx , url_str , config , resp_obj )
149+ except Exception :
150+ pass
151+ return resp_obj
152+
153+ # --- Helpers to parse the general response payload and build a Response-like object ---
154+ def _parse_general_response (self , sender_public_key : Optional [Any ], payload : bytes , request_nonce_b64 : str , url_str : str , config : SimplifiedFetchRequestOptions ):
155+ if not payload :
156+ return None
157+ # Try binary format first (Go/TS protocol)
158+ resp = self ._try_parse_binary_general (sender_public_key , payload , request_nonce_b64 , url_str , config )
159+ if resp is not None :
160+ return resp
161+ # Fallback to JSON structure used by the simplified Python transport
162+ try :
163+ txt = payload .decode ('utf-8' , errors = 'strict' )
164+ obj = json .loads (txt )
165+ status = int (obj .get ('status_code' , 0 ))
166+ headers = obj .get ('headers' , {}) or {}
167+ body_str = obj .get ('body' , '' )
168+ body_bytes = body_str .encode ('utf-8' )
169+ return self ._build_response (url_str , config .method or 'GET' , status , headers , body_bytes )
170+ except Exception :
171+ return None
172+
173+ def _try_parse_binary_general (self , sender_public_key : Optional [Any ], payload : bytes , request_nonce_b64 : str , url_str : str , config : SimplifiedFetchRequestOptions ):
174+ try :
175+ if len (payload ) < 33 : # require nonce + at least one byte for status code varint
176+ return None
177+ reader = _BinaryReader (payload )
178+ response_nonce = reader .read_bytes (32 )
179+ response_nonce_b64 = base64 .b64encode (response_nonce ).decode ()
180+ if response_nonce_b64 != request_nonce_b64 :
181+ return None
182+ # Save identity key and mutual auth support flag
183+ if sender_public_key is not None :
184+ try :
185+ self .peers [urllib .parse .urlparse (url_str ).scheme + '://' + urllib .parse .urlparse (url_str ).netloc ].identity_key = getattr (sender_public_key , 'to_der_hex' , lambda : str (sender_public_key ))()
186+ self .peers [urllib .parse .urlparse (url_str ).scheme + '://' + urllib .parse .urlparse (url_str ).netloc ].supports_mutual_auth = True
187+ except Exception :
188+ try :
189+ self .peers [urllib .parse .urlparse (url_str ).scheme + '://' + urllib .parse .urlparse (url_str ).netloc ].supports_mutual_auth = True
190+ except Exception :
191+ pass
192+ status_code = reader .read_varint32 ()
193+ n_headers = reader .read_varint32 ()
194+ headers : Dict [str , str ] = {}
195+ for _ in range (n_headers ):
196+ key = reader .read_string ()
197+ val = reader .read_string ()
198+ headers [key ] = val
199+ # Add back server identity key if available
200+ if sender_public_key is not None :
201+ try :
202+ headers ['x-bsv-auth-identity-key' ] = getattr (sender_public_key , 'to_der_hex' , lambda : str (sender_public_key ))()
203+ except Exception :
204+ headers ['x-bsv-auth-identity-key' ] = str (sender_public_key )
205+ body_len = reader .read_varint32 ()
206+ body_bytes = b''
207+ if body_len > 0 :
208+ body_bytes = reader .read_bytes (body_len )
209+ return self ._build_response (url_str , config .method or 'GET' , int (status_code ), headers , body_bytes )
210+ except Exception :
211+ return None
212+
213+ def _build_response (self , url_str : str , method : str , status : int , headers : Dict [str , str ], body : bytes ):
214+ resp_obj = requests .Response ()
215+ resp_obj .status_code = int (status )
216+ try :
217+ from requests .structures import CaseInsensitiveDict
218+ resp_obj .headers = CaseInsensitiveDict (headers or {})
219+ except Exception :
220+ resp_obj .headers = headers or {}
221+ resp_obj ._content = body or b''
222+ resp_obj .url = url_str
223+ try :
224+ req = requests .Request (method = method or 'GET' , url = url_str )
225+ resp_obj .request = req .prepare ()
226+ except Exception :
227+ pass
228+ resp_obj .reason = str (status )
229+ return resp_obj
115230
116231 def send_certificate_request (self , ctx : Any , base_url : str , certificates_to_request ):
117232 """
@@ -121,12 +236,12 @@ def send_certificate_request(self, ctx: Any, base_url: str, certificates_to_requ
121236 base_url_str = f"{ parsed_url .scheme } ://{ parsed_url .netloc } "
122237 if base_url_str not in self .peers :
123238 transport = SimplifiedHTTPTransport (base_url_str )
124- peer = Peer (
239+ peer = Peer (PeerOptions (
125240 wallet = self .wallet ,
126241 transport = transport ,
127242 certificates_to_request = self .requested_certificates ,
128243 session_manager = self .session_manager
129- )
244+ ))
130245 auth_peer = AuthPeer ()
131246 auth_peer .peer = peer
132247 self .peers [base_url_str ] = auth_peer
@@ -223,7 +338,6 @@ def _write_body(self, buf, body):
223338 self ._write_varint (buf , 0xFFFFFFFFFFFFFFFF ) # -1
224339
225340 def _write_varint (self , writer : bytearray , value : int ):
226- import struct
227341 writer .extend (struct .pack ('<Q' , value ))
228342
229343 def _write_bytes (self , writer : bytearray , b : bytes ):
@@ -354,6 +468,64 @@ def _set_payment_header(self, config, payment_info, derivation_suffix, tx_b64):
354468 config .headers = {}
355469 config .headers ["x-bsv-payment" ] = payment_info_json
356470
471+
472+ class _BinaryReader :
473+ """
474+ Minimal binary reader compatible with the Go util.Reader used by AuthHTTP.
475+ Supports:
476+ - read_bytes(n)
477+ - read_varint() / read_varint32()
478+ - read_string() where string is prefixed with varint length and -1 encoded as 0xFFFFFFFFFFFFFFFF
479+ """
480+
481+ def __init__ (self , data : bytes ):
482+ self ._data = data
483+ self ._pos = 0
484+
485+ def _require (self , n : int ):
486+ if self ._pos + n > len (self ._data ) or n < 0 :
487+ raise ValueError ("read past end of data" )
488+
489+ def read_bytes (self , n : int ) -> bytes :
490+ self ._require (n )
491+ b = self ._data [self ._pos :self ._pos + n ]
492+ self ._pos += n
493+ return b
494+
495+ def read_varint (self ) -> int :
496+ self ._require (1 )
497+ first = self ._data [self ._pos ]
498+ self ._pos += 1
499+ if first < 0xFD :
500+ return first
501+ if first == 0xFD :
502+ self ._require (2 )
503+ val = struct .unpack_from ('<H' , self ._data , self ._pos )[0 ]
504+ self ._pos += 2
505+ return val
506+ if first == 0xFE :
507+ self ._require (4 )
508+ val = struct .unpack_from ('<I' , self ._data , self ._pos )[0 ]
509+ self ._pos += 4
510+ return val
511+ # 0xFF
512+ self ._require (8 )
513+ val = struct .unpack_from ('<Q' , self ._data , self ._pos )[0 ]
514+ self ._pos += 8
515+ return val
516+
517+ def read_varint32 (self ) -> int :
518+ return int (self .read_varint () & 0xFFFFFFFF )
519+
520+ def read_string (self ) -> str :
521+ length = self .read_varint ()
522+ NEG_ONE = 0xFFFFFFFFFFFFFFFF
523+ if length == 0 or length == NEG_ONE :
524+ return ""
525+ b = self .read_bytes (int (length ))
526+ return b .decode ('utf-8' , errors = 'strict' )
527+
528+
357529# --- P2PKH lockingScript生成関数 ---
358530def p2pkh_locking_script_from_pubkey (pubkey_hex : str ) -> str :
359531 """
@@ -375,4 +547,4 @@ def p2pkh_locking_script_from_pubkey(pubkey_hex: str) -> str:
375547 + b'88' # OP_EQUALVERIFY
376548 + b'ac' # OP_CHECKSIG
377549 )
378- return binascii .hexlify (script ).decode ()
550+ return binascii .hexlify (script ).decode ()
0 commit comments