1010from __future__ import annotations
1111
1212import http .client
13+ import base64
1314import os
1415from pathlib import Path
16+ import subprocess
1517import tempfile
1618import urllib .parse
1719import urllib .request
@@ -63,8 +65,68 @@ def _iter_pkt_stream(source) -> Iterator[bytes]:
6365 yield payload
6466
6567
68+ def _request_headers (url : str , extra : dict [str , str ] | None = None ) -> dict [str , str ]:
69+ headers = {"User-Agent" : "pythongit/0.1" }
70+ if extra :
71+ headers .update (extra )
72+ auth = _auth_header_for_url (url )
73+ if auth :
74+ headers ["Authorization" ] = auth
75+ return headers
76+
77+
78+ def _auth_header_for_url (url : str ) -> str | None :
79+ user , password = _credentials_for_url (url )
80+ if not user or not password :
81+ return None
82+ raw = f"{ user } :{ password } " .encode ("utf-8" )
83+ return "Basic " + base64 .b64encode (raw ).decode ("ascii" )
84+
85+
86+ def _credentials_for_url (url : str ) -> tuple [str , str ]:
87+ parsed = urllib .parse .urlsplit (url )
88+ scheme = parsed .scheme
89+ host = parsed .hostname or ""
90+ user = urllib .parse .unquote (parsed .username or "" )
91+ password = urllib .parse .unquote (parsed .password or "" )
92+ if user and password :
93+ return user , password
94+
95+ try :
96+ from . import bridges
97+ fields = {"protocol" : scheme , "host" : host }
98+ creds = bridges .credential_fill (fields , use_external = False )
99+ except Exception :
100+ creds = {}
101+ user = user or creds .get ("username" , "" )
102+ password = password or creds .get ("password" , "" )
103+ if user and password :
104+ return user , password
105+
106+ if scheme == "https" and host in {"github.com" , "www.github.com" }:
107+ token = os .environ .get ("GH_TOKEN" ) or os .environ .get ("GITHUB_TOKEN" ) or _gh_auth_token (host )
108+ if token :
109+ return user or "x-access-token" , token
110+ return "" , ""
111+
112+
113+ def _gh_auth_token (host : str ) -> str :
114+ try :
115+ proc = subprocess .run (
116+ ["gh" , "auth" , "token" , "--hostname" , host ],
117+ text = True ,
118+ capture_output = True ,
119+ timeout = 5 ,
120+ )
121+ except (OSError , subprocess .TimeoutExpired ):
122+ return ""
123+ if proc .returncode != 0 :
124+ return ""
125+ return proc .stdout .strip ()
126+
127+
66128def _get (url : str ) -> bytes :
67- req = urllib .request .Request (url , headers = { "User-Agent" : "pythongit/0.1" } )
129+ req = urllib .request .Request (url , headers = _request_headers ( url ) )
68130 with urllib .request .urlopen (req ) as r :
69131 return r .read ()
70132
@@ -74,11 +136,7 @@ def _post(url: str, body: bytes, content_type: str, accept: str) -> bytes:
74136 url ,
75137 data = body ,
76138 method = "POST" ,
77- headers = {
78- "User-Agent" : "pythongit/0.1" ,
79- "Content-Type" : content_type ,
80- "Accept" : accept ,
81- },
139+ headers = _request_headers (url , {"Content-Type" : content_type , "Accept" : accept }),
82140 )
83141 with urllib .request .urlopen (req ) as r :
84142 return r .read ()
@@ -89,11 +147,7 @@ def _post_stream(url: str, body: bytes, content_type: str, accept: str):
89147 url ,
90148 data = body ,
91149 method = "POST" ,
92- headers = {
93- "User-Agent" : "pythongit/0.1" ,
94- "Content-Type" : content_type ,
95- "Accept" : accept ,
96- },
150+ headers = _request_headers (url , {"Content-Type" : content_type , "Accept" : accept }),
97151 )
98152 return urllib .request .urlopen (req )
99153
@@ -118,9 +172,8 @@ def _post_with_pack_file(
118172 try :
119173 conn .putrequest ("POST" , target )
120174 conn .putheader ("Host" , host_header )
121- conn .putheader ("User-Agent" , "pythongit/0.1" )
122- conn .putheader ("Content-Type" , content_type )
123- conn .putheader ("Accept" , accept )
175+ for key , value in _request_headers (url , {"Content-Type" : content_type , "Accept" : accept }).items ():
176+ conn .putheader (key , value )
124177 conn .putheader ("Content-Length" , str (len (prefix ) + pack_size ))
125178 conn .endheaders ()
126179 if prefix :
0 commit comments