Repository navigation
Added
syshttp.client: an HTTP client on the standard library, so that no feature
of syshttp needsrequests(which onlysyshttp.autopatchpatches, in a
program that imports it).HTTPClient(transport=None, *, headers, timeout, deadline, user_agent, env, proxymap, ssl_context, proxy_ssl_context, verify, cookies, tokens, auth_provider, retries, backoff, status_forcelist, cookie_hosts)sends through a transport and follows redirects with one rule
for credentials (Authorization,CookieandProxy-Authorizationgo to the
origin they were given for and to no other scheme, host or port). The defaultHTTPTransport(onhttp.client,
with a per-thread connection pool, framed bodies,gzip/deflate
decoding and a bounded reply size) routes every request and redirect hop by
proxy.proxy_map(env=env)(orproxymap=): the first entry that accepts a
connection, anhttporhttpsproxy (CONNECT; anhttpsproxy over TLS
with the origin's TLS inside it), a URL-prefix gateway, a proxy's
credential to the proxy alone; and verifies with the caller'sssl_context,
else theCA_BUNDLEbundle orverify=<path>, else the system's context
(proxy_ssl_contextfor anhttpsproxy or gateway). Cookies (RFC 6265,
per origin, on every hop, in a jar by host withcookies=), tokens,_PY
hooks,auth_providerand retries (Retry-Afterhonoured, a status that
outlasts them returned) work as they did on the session.HTTPResponse
reads its body whole ascontent,text()andjson().syshttp.fetch()(defined insyshttp.client, re-exported from the root
without importing it):fetch(url, method="GET", *, headers, data, json, timeout, client, transport, reauth, **client_options)returns the
HTTPResponsewith its body read, whatever the status; a givenclientis
left open and a new one is closed;reauthruns once after a 401 or 403.- The transport protocol is public:
Transport(send()andclose()),
PreparedRequestandprepare_request()give another transport (on
requests,httpx, or a fake) the checks and body framing of syshttp's own.
SYSHTTP_DEFAULT_TRANSPORT(module:attror<file>.py:attr, read under the
prefixes) names the transport of a client given none; a value that cannot be
used isConfigurationError.hooks.load_object()is the loader it shares
with the_PYhooks. syshttp.exceptions:SysHTTPError(the base of what goes wrong at run
time),ConfigurationError(aValueError),HookError(aRuntimeError)
andResponseError(aValueError), exported from the root. A malformed
setting, a hook that cannot be loaded, a failing hook and a body that is not
JSON raise them; every one is still theValueErrororRuntimeErrorit
was, so existing handlers keep working. A caller's own mistake stays a plain
ValueError/TypeError.- Every public module declares
__all__(proxyalso lists its setting-name
constants,PROXYandDEFAULT_AUTH_HEADERamong them). syshttp.env.HTTPEnv(prefixes=None, environ=None): every setting is a
variable with a full name -- syshttp's own carrySYSHTTP_
(SYSHTTP_PROXY,SYSHTTP_HOME,SYSHTTP_HOOKS_<KEY>,
SYSHTTP_IMPORTPATCH), conventional ones keep theirs (HTTPS_PROXY,
NO_PROXY,HTTP_PROXY_AUTH,CA_BUNDLE) -- read as<PREFIX><NAME>for
each prefix ofSYSHTTP_PREFIXin turn, a:-separated list used
literally (default: one empty prefix, the names as written). The first
prefix that sets a variable wins, each variable on its own:
SYSHTTP_PREFIX=MYTOOL_reads onlyMYTOOL_<NAME>,MYTOOL_:falls back
to the unprefixed names.find()returns the variable found with its
value, for messages. Every function that reads a setting takesenv=,
new_session()and thefetch*_autohelpers included.CA_BUNDLE(under the prefixes) names the CA bundle a session's
verify=Trueuses; sessions no longer read requests' own
REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE.- The system's proxy configuration is the last proxy source, decided for the
URL being requested: WinHTTP on Windows, CFNetwork on macOS, libproxy or the
desktop's settings elsewhere, PAC scripts and WPAD included (through
proxylib, a new dependency). It comes afterSYSHTTP_PROXYand the
*_PROXYvariables, after theNO_PROXYcheck, and is always spoken to as
connect.SYSHTTP_SYSTEM_PROXY=0turns it off.proxy.plan(),
proxy_url(),resolve()andproxy_authorization()take the request URL
(url=/target=).proxy.configured_proxy_url()gives the
environment's answer alone. Thepacextra adds a JavaScript engine for PAC
scripts proxylib evaluates itself (Linux without libproxy). syshttp.autopatch:SYSHTTP_IMPORTPATCH=1(or:-separated names:
requests,urllib,httpx,aiohttp; read under the prefixes like
every setting) patches those libraries as they are imported,
in any program of the environment, so requests without a proxy or TLS
setting of the caller's route by syshttp and verify with the system's
context. Through importpatch, a new dependency, and its entry points;
patch()/unpatch()do it from code.- The system's answer is a list (
PROXY a; PROXY b; DIRECT): the first proxy
that accepts a connection is used (proxylib'sfirst_working_proxy), and a
DIRECT reached first is direct. No reachable proxy and no DIRECT raises
proxy.NoReachableProxyError(aConnectionError, exported from the
root); a session raisesrequests.exceptions.ProxyErrorfor it. new_session(ssl_context=..., proxy_ssl_context=...)(and the same on the
fetch*_autohelpers): a caller'sssl.SSLContextis used as given for
every https origin, or for anhttps://proxy. That covers mutual TLS,
ciphers and protocol versions. It cannot be combined with averifyother
thanTrue.hooks.wrapper_environment(): theSYSHTTP_HOOK_*variables a_CURL
wrapper run receives.jar.default_root(): the per-user state directory used when
SYSHTTP_HOMEis unset.syshttp.__version__.- Hosts that share cookies:
SYSHTTP_COOKIE_HOSTS(hosts split by,,
groups by;) ornew_session(cookie_hosts=...)declares names that reach
one service. In a group, a cookie a member sets for itself or another
member belongs to the first host, every member's requests carry the
group's cookies, and any member can delete them; aDomainoutside the
group is still judged on the host that sent it.cookies.host_aliases()
builds the mapping, whichset_cookie_specs,merge_set_cookie_headers
andhost_only_policytake asaliases=. - The shipped header and the settings page list every variable syshttp
reads under its own name (the proxy family,REQUEST_METHOD, the CA bundle
variables, the state-directory variables, importpatch's and proxylib's).
Changed
-
syshttp.fetchis a function, not a module:fetch()replaces the helpers
below._PYhooks are called ashook(client, method, url, kwargs)with the
keyword arguments ofHTTPClient.request(), and may return an
HTTPResponse;auth.AuthProvider.ensure(client, url)takes the client.
CA_BUNDLEis the bundle a client'sverify=Trueuses.cookies.pykeeps
the Netscape rows,set_cookie_specs,cookie_appliesandhost_aliases
only; the client keeps its cookies as those rows. -
Routing is proxylib 2's (
proxylib>=2.0.2,<3; 2.0.1 breaks pip's cached
requests).proxy.proxy_map(proxy, noproxy, proxy_user, *, env)is the settings as one proxylib proxy map,
read at each lookup;proxy.configured_proxy()isSYSHTTP_PROXYas a
proxylibProxy;proxy.system_map()is the system's configuration
(proxylib.auto_proxy). The*_PROXYvariables are read per scheme
(lowercase first, httpoxy-safe),NO_PROXYtakes CIDR entries, and curl's
-x ""is direct.SYSHTTP_PROXY_AUTHgoes toSYSHTTP_PROXYonly and
HTTP_PROXY_AUTHto the machine's proxies only. -
A list from the system (
PROXY a; PROXY b; DIRECT) is tried in order by the
session, httpx and aiohttp (proxylib's failover), replacing the probe of
the entry above; urllib takes the first reachable entry. No proxy
connecting and no DIRECT is the client's own connection error;
NoReachableProxyErroris proxylib's. -
The session's
ProxyAdapteris proxylib'sProxyMapAdapterwith the
system's TLS;adapter.route(url)returns the entries it will try.
proxies=given tonew_sessionare requests' own, with no syshttp
credential added. -
syshttp.autopatchroutes throughproxylib.patch()and patches TLS only;
requests' TLS is a pool hook on everyHTTPAdapter. A caller's own
proxies andtrust_env = Falsestill win over syshttp (proxylib 2.0.2's
rule). aiohttp is not offered
URL-prefix gateways. New targetpip(pip._vendor.requests, proxylib
2.0.1'spiptarget): pip's adapters route through the map unless pip is
given--proxyor--no-proxy-env; pip keeps its own TLS context. -
The jar constructors (
FileCookieJar,FileTokenJar) no longer create
their directory; it is created at the first write. -
A session returns the last response when a status in
status_forcelist
outlasts its retries, as plain requests does; it used to raise
requests.exceptions.RetryError, which the fetch helpers (-> (status, text)) passed on. The*_autohelpers takenew_session's options as
**session_options(every keyword they had still works) and their
timeoutmay be a(connect, read)pair. -
Every dependency's floor is its newest release that supports Python 3.9:
thesessionextra requiresrequests>=2.32.5andurllib3>=2.6.3(was
requests>=2.25), andsslcontext>=2.1.0. Below requests 2.32.3 a caller's
ssl_contextwas silently ignored, urllib3 1.26 raises a bareValueError
for an https IP-literal host, and urllib3 up to 2.2 leaks the socket of a
failed TLS handshake through an https proxy. On Python 3.9, botocore pinsurllib3<1.27, sosyshttp[session]
cannot share an environment with it there. -
The header says that jars, tokens,
_PYhooks andauth_providerapply to
session.requestand not to a baresession.send(), which gets the proxy
decision and TLS only. -
new_session()no longer callsurllib3.disable_warnings, which silenced
InsecureRequestWarningfor the whole process; withverify=Falseurllib3
warns as it does for any requests session. The undocumented
syshttp.warningsmodule is gone.
Removed
syshttp.session(new_session()and itsrequestsadapter), the
sessionextra (and itsrequestsandurllib3floors;requestsstays a
development dependency for the tests ofsyshttp.autopatch), and the helpers
of the oldsyshttp.fetchmodule:request_text,request_json,
request_with_reauth,request_text_autoandrequest_json_auto. Their
replacements areHTTPClientandfetch(). A per-requestverify=and
cert=are gone with them: build a client withverify=or an
ssl_contextthat holds the identity.cookies.apply_to_session,
merge_set_cookie_headersandhost_only_policy, which only arequests
session used, are gone too.proxy.proxy_url,configured_proxy_url,system_proxy,
system_proxy_map,first_reachable,PROBE_MAX_AGE,PROBE_TIMEOUT,
resolve,proxy_authorization,configured_authorization,userinfo,
basic_authorization,strip_auth,redact,prefix_url,
should_bypass,bypassed_by,no_proxy_from_env,proxies_from_env,
planandProxyPlan:proxy_map()and proxylib'sProxyreplace them.
Changed (earlier)
- The package stands alone as
syshttp. It was previously thehttplib
library of the dotagentsnetoverlay, which shipped with a curl-compatible
command (httplib.cli,python -m httplib) that is now thecurlike
package. Imports move fromhttplibtosyshttp, and settings from the
dotagents names to the prefixed ones:AGENTS_PROXY→SYSHTTP_PROXY,
AGENTS_PROXY_AUTH→SYSHTTP_PROXY_AUTH,AGENTS_PROXY_AUTH_HEADER→
SYSHTTP_PROXY_AUTH_HEADER,AGENTS_PROXY_TYPE→SYSHTTP_PROXY_TYPE,
AGENTS_HOME→SYSHTTP_HOME,NET_HOOKS_<KEY>→SYSHTTP_HOOKS_<KEY>,
NET_HOOK_*→SYSHTTP_HOOK_*. A tool's own namespace is a prefix:
SYSHTTP_PREFIX=AGENTS_readsAGENTS_SYSHTTP_PROXY,
AGENTS_SYSHTTP_HOOKS_<KEY>and so on. - The system's TLS client by default:
tls.new_os_context()(and so
os_ssl_context()and every session withverify=True) returns
sslcontext.system_context()(sslcontext 2 is a new dependency).
Certificates are verified by the operating system on Python 3.10+
(CryptoAPI on Windows, the Security framework on macOS, OpenSSL over the
system store elsewhere), by OpenSSL over the default store on 3.9, and a
server that asks for a client certificate gets one from the OS store,
chosen as a browser chooses.new_os_context(client_certificates=False)
offers none, for a caller loading its own. Acafile/capathstill
replaces the store with a plainsslcontext. - A session without caller
proxiesdecides the proxy for every request and
every redirect hop, from the environment as it is then and the system's
answer for that URL. A direct decision is final: requests'trust_envcannot
re-add a proxy.adapter.route(url)shows the decision. - Without
SYSHTTP_HOME, the jars live in the per-user state directory
instead of the dotagents store directory. proxy.resolve()andproxy.proxy_authorization(): the keywordenv: bool
is nowconfigured: bool.hooks.hooks_from_env(),matching(),skipped()andcall_py_hooks()
takeenv: HTTPEnv(keyword-only) instead of a mappingenviron.
hooks.PREFIX,SKIP_ENV,URL_ENV,KEY_ENVandCURL_ENVare
replaced by the setting namesHOOKS,HOOK_SKIP,HOOK_URL,HOOK_KEY
andHOOK_CURL.proxy.AUTH_VARS,AUTH_HEADER_VARandTYPE_VARare replaced by the
setting namesPROXY_AUTH,PROXY_AUTH_HEADERandPROXY_TYPE.
Fixed
- A by-host cookie file keeps a cookie its host set for a parent domain
(Domain=example.comfromwww.example.com); it was never saved. - The proxy credential reaches a proxy whose host is configured in upper
case. requests lower-cases the proxy URL it hands toproxy_headers, so the
credential used to be dropped and the proxy answered 407. Set-Cookieis read as RFC 6265 section 5.2 reads it, not through
http.cookies.SimpleCookie: a value with a space or JSON is kept, an
unknown attribute (Priority,Partitioned) is no longer a cookie of its
own,Max-AgebeatsExpires, and a header with noname=valueis
skipped. ADomainwithout a leading dot covers subdomains. A cookie with
noPathgets the directory of the request path
(set_cookie_specs(..., request_path=)).- A session holds one copy of each cookie.
merge_set_cookie_headersno
longer re-sets what requests already stored (it used to add a second copy
with path/and a dotless domain); it applies only the deletions requests
misses, for redirect hops too. - A cookie set without
Domainstays on its host: rows loaded from a jar and
cookies a server sets are no longer sent to subdomains
(new_sessionusescookies.host_only_policy()). - A cookie for a single-label host (
localhost,wiki) is loaded from its
jar file, and is saved under its own name instead oflocalhost.local. FileCookieJarkeepsHttpOnlyand the include-subdomains flag.load_netscapekeeps a row whose value is empty, and an expiry written
with a non-ASCII digit is a session cookie instead of aValueError.
retry_afterreturnsNonefor such a value, and reads an HTTP date
written without a zone (asctime) as UTC.- A cookie expiry beyond 400 days is cut to 400 days.
- A hook whose
_PY/_CURLtarget is blank is ignored instead of failing
every request, andHTTPEnv.familystrips values asgetdoes. - Hook KEYs are compared without regard to case:
HOOK_SKIP=mykeydid not
suppressMYKEY, the form Windows holds.Hook.keyandskipped()are
upper-case. - A
<file>.py:callablehook's module is loaded once per process, not on
every request (its module-level state was reset each time), and two files
with one name no longer share a module name. request_text_autoandrequest_json_autoretry a 408 by default, as
new_sessiondoes (their own list had dropped it), and close the session
they built instead of leaking its connection pool. The five fetch helpers
have docstrings.new_session(user_agent=...)sets the User-Agent; the default
python-requests/xwas sent whatever was given.- The guards that keep a hook or
auth_provider.ensurefrom running again for
its own requests are per thread: while one thread was inside a hook, the
other threads' requests skipped hooks andensure. - A request-level
verify=(False, or a bundle) besidessl_context, which
was silently ignored, raisesValueErroras the same value does at session
level.CA_BUNDLEis not a request-level value, and a session with an
ssl_contextdoes not consult it. - The import patch replaces requests 2.32.3's own default context (a certifi
bundle) in a pool, as it does for the later versions that leave it out. - Token and cookie files are replaced whole by a rename, so a reader never sees
an empty or half-written file and concurrent savers no longer truncate each
other's file. SYSHTTP_HOME=~/jarsis expanded instead of creating a directory named~.- Jar files are written with LF on Windows too, as curl writes them.
- A session whose cookie jar cannot be read (an error while iterating
session.cookies) no longer saves an empty list over the host's cookies, and
writes a host's file only when its cookies changed instead of after every
response.
Security
- The client closes six gaps found in review. A
Set-Cookiewith a control
character in it is ignored whole (RFC 6265bis 5.6),load_netscapeends a
line at a newline only (it ended one at\x0b,\x0c,\x1c-\x1eand
\x85too, so a value could become a whole row for another domain),
save_netscapewrites no row with a control character or a tab in a field,
and a by-host jar file gives the client only the rows that cover its host or
group. A plain-http response can no longer set a Secure cookie, or set,
replace or delete one that is stored (RFC 6265bis 5.7; the loopback hosts
count as secure, as in curl). A redirect to another scheme, host or port
drops a caller'sCookieandProxy-Authorizationheader with
Authorization.HTTPTransport.send(stream=False)and
HTTPResponse.buffered()hold a decoded reply toMAX_REPLY_BYTES(a
kilobyte of gzip decoded to tens of megabytes), and the too-large error names
the URL as scheme, host, port and path, without its query. Anhttps:origin
is no longer sent through a plainprefix+httpgateway, which would carry its
tokens and Secure cookies in clear text: the entry is skipped, and with
nothing else left the request raisesproxylib.UnsupportedProxyError. - A
Set-Cookiecan no longer name a domain its host does not own. Any
host could set or delete another host's cookie (Domain=bank.examplefrom
evil.example) or one for a top level domain; such a header is now skipped
on a set and on a deletion, in the session, inFileCookieJarand in
set_cookie_specs(which curlike's-cuses). A public suffix is
recognised without the Public Suffix List (a name with no dot, or a
co.uk-style registry). - Token and cookie files are created
0600, and the directories this library
creates0700, instead of world-readable under a usual umask. - A client certificate passed as
cert=is no longer loaded into the shared OS
context (or into the caller'sssl_context), where every later session, and
every other user of that context, presented it too. It gets a context of the
session's own;cert=besidessl_contextraisesValueError. - A URL-prefix gateway is spoken to with
proxy_ssl_context(or the system's
verifier), no longer with thessl_contextand client certificate meant for
the origin. - A token, or a proxy credential, containing CR, LF or NUL raises a
ValueErrorthat names the jar key or variable instead of requests'
InvalidHeadercarrying the whole secret. - No setting can be chosen by a CGI client unless the program reads the
unprefixed names: syshttp's own all startSYSHTTP_, which a request
header never becomes, and a CGI program sets a prefix list without the
empty entry so thatHTTP_PROXYandHTTP_PROXY_AUTHare not read. - A failing hook's message names its URL as scheme, host, port and path, no
longer with the userinfo, query and fragment (user:pass@,
?access_token=).
Full Changelog: https://github.com/jose-pr/syshttp/commits/v0.1.0