syshttp 0.1.0__tar.gz → 0.1.1__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.
- {syshttp-0.1.0 → syshttp-0.1.1}/CHANGELOG.md +34 -1
- {syshttp-0.1.0 → syshttp-0.1.1}/PKG-INFO +37 -1
- {syshttp-0.1.0 → syshttp-0.1.1}/README.md +31 -0
- syshttp-0.1.1/docs/api/negotiate.md +3 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/index.md +1 -1
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/settings.md +54 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/mkdocs.yml +1 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/pyproject.toml +19 -5
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/AGENTS.md +135 -6
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/__init__.py +12 -1
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_client.py +4 -1
- syshttp-0.1.1/src/syshttp/client/_handshake.py +152 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_transport.py +185 -13
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/exceptions.py +24 -1
- syshttp-0.1.1/src/syshttp/negotiate.py +327 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/client_wire.py +287 -3
- syshttp-0.1.1/tests/test_negotiate_auth.py +701 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/.gitignore +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/LICENSE +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/autopatch.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/client.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/cookies.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/env.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/exceptions.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/hooks.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/jar.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/proxy.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/api/tls_retry.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/docs/changelog.md +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/_fileio.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/_parse.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/auth.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/autopatch.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/__init__.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_body.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_context.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_deadline.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_default.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_fetch.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_follow.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_headers.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_pool.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_prepare.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_response.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_route.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_settings.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_state.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_status.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/client/_tlstunnel.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/cookies.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/env.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/hooks.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/jar.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/proxy.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/py.typed +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/retry.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/src/syshttp/tls.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/client_fakes.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/conftest.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/data/ca.pem +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/data/client-rsa.key +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/data/client-rsa.pem +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/data/server.key +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/data/server.pem +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_autopatch.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_credential_redirects.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_deadline.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_default.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_default_transport.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_errors.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_guards.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_paths.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_peer_text.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_redirect_origin.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_redirects.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_reply_bounds.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_reply_limits.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_response.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_routing.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_secure_cookies.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_state.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_tls.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_urls.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_client_write_redirects.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_cookie_hosts.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_cookie_injection.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_cookies.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_edges.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_env.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_exceptions.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_fetch.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_jar_files.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_jars_proxy.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_os_context.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_proxy.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_proxy_auth.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_retry.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_switches.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_system_proxy.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_transport_body.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_transport_pool.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_transport_prepare.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_transport_send.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_transport_tls.py +0 -0
- {syshttp-0.1.0 → syshttp-0.1.1}/tests/test_url_hooks.py +0 -0
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-11-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `syshttp.negotiate`: Negotiate (SPNEGO) and NTLM authentication for the
|
|
15
|
+
default transport, so `HTTPClient`, `fetch()` and `HTTPTransport` answer a
|
|
16
|
+
`401` with `WWW-Authenticate: Negotiate` or `NTLM` and a `407` with
|
|
17
|
+
`Proxy-Authenticate` (a `CONNECT` the proxy challenges included), leg after
|
|
18
|
+
leg on the connection the challenge came on, with `Connection: keep-alive`.
|
|
19
|
+
It is off until `SYSHTTP_NEGOTIATE` (servers) or `SYSHTTP_PROXY_NEGOTIATE`
|
|
20
|
+
(proxies) names a host (`*.suffix` and `*` allowed), or `Negotiate(hosts,
|
|
21
|
+
proxies, *, username, password, plain_http, max_legs, max_token, env)` is
|
|
22
|
+
passed as `negotiate=` to `HTTPClient` or `HTTPTransport`. The identity is the
|
|
23
|
+
Windows logon session (SSPI) or the Kerberos credential cache, or
|
|
24
|
+
`SYSHTTP_NEGOTIATE_USER` and `SYSHTTP_NEGOTIATE_PASSWORD`; a password is in no
|
|
25
|
+
`repr()`, log line or message. A token is not sent over plain http to a host
|
|
26
|
+
that is not loopback unless `SYSHTTP_NEGOTIATE_PLAIN_HTTP` allows it, which
|
|
27
|
+
is logged as a weakening; `SYSHTTP_NEGOTIATE_MAX_LEGS` (default 5) and
|
|
28
|
+
`SYSHTTP_NEGOTIATE_MAX_TOKEN` (default 65536) bound a handshake. A server's
|
|
29
|
+
closing token is checked where the mechanism has mutual authentication. Skipped
|
|
30
|
+
for a request that names its own `Authorization`, a proxy with a credential of
|
|
31
|
+
its own, a body that cannot be sent twice and a gateway.
|
|
32
|
+
- The extras `negotiate` (pyspnego: SSPI on Windows, NTLM in Python) and
|
|
33
|
+
`kerberos` (pyspnego with `gssapi`, for the credential cache on Linux and
|
|
34
|
+
macOS). `import syshttp` and a default install load and need neither.
|
|
35
|
+
- `NegotiationError` (a handshake that failed: credentials refused, a token that
|
|
36
|
+
is not valid or too large, a connection closed or more legs than allowed, a
|
|
37
|
+
handshake that would cross plain http) and `NegotiateUnavailableError` (also an
|
|
38
|
+
`ImportError`: the extra is missing or does not import), both under
|
|
39
|
+
`SysHTTPError` and exported from the root; `has_negotiate()` says whether the
|
|
40
|
+
library imports.
|
|
41
|
+
|
|
10
42
|
## [0.1.0] - 2026-10-20
|
|
11
43
|
|
|
12
44
|
### Added
|
|
@@ -339,5 +371,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
339
371
|
longer with the userinfo, query and fragment (`user:pass@`,
|
|
340
372
|
`?access_token=`).
|
|
341
373
|
|
|
342
|
-
[Unreleased]: https://github.com/jose-pr/syshttp/compare/v0.1.
|
|
374
|
+
[Unreleased]: https://github.com/jose-pr/syshttp/compare/v0.1.1...HEAD
|
|
375
|
+
[0.1.1]: https://github.com/jose-pr/syshttp/compare/v0.1.0...v0.1.1
|
|
343
376
|
[0.1.0]: https://github.com/jose-pr/syshttp/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: syshttp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: HTTP plumbing that follows the system: the system proxy (PAC, WPAD) and the OS certificate verifier by default, proxy credentials and prefix gateways, Netscape cookie jars, OS trust-store TLS, URL hooks, and an HTTP client on the standard library
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/syshttp
|
|
6
6
|
Project-URL: Documentation, https://jose-pr.github.io/syshttp/
|
|
@@ -29,6 +29,7 @@ Requires-Dist: black; extra == 'dev'
|
|
|
29
29
|
Requires-Dist: build; extra == 'dev'
|
|
30
30
|
Requires-Dist: httpx; extra == 'dev'
|
|
31
31
|
Requires-Dist: mypy<2; extra == 'dev'
|
|
32
|
+
Requires-Dist: pyspnego<0.13,>=0.12.0; extra == 'dev'
|
|
32
33
|
Requires-Dist: pytest; extra == 'dev'
|
|
33
34
|
Requires-Dist: requests; extra == 'dev'
|
|
34
35
|
Requires-Dist: types-requests; extra == 'dev'
|
|
@@ -36,6 +37,10 @@ Provides-Extra: docs
|
|
|
36
37
|
Requires-Dist: mkdocs; extra == 'docs'
|
|
37
38
|
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
38
39
|
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
40
|
+
Provides-Extra: kerberos
|
|
41
|
+
Requires-Dist: pyspnego[kerberos]<0.13,>=0.12.0; extra == 'kerberos'
|
|
42
|
+
Provides-Extra: negotiate
|
|
43
|
+
Requires-Dist: pyspnego<0.13,>=0.12.0; extra == 'negotiate'
|
|
39
44
|
Provides-Extra: pac
|
|
40
45
|
Requires-Dist: proxylib[pac]<3,>=2.0.2; extra == 'pac'
|
|
41
46
|
Description-Content-Type: text/markdown
|
|
@@ -101,6 +106,14 @@ it. Documentation: <https://jose-pr.github.io/syshttp/>.
|
|
|
101
106
|
all of the above, on `http.client` and `ssl` alone. Its transport is
|
|
102
107
|
replaceable: any object with `send()` (a `requests` or `httpx` one, a fake
|
|
103
108
|
in a test), chosen per client or by `SYSHTTP_DEFAULT_TRANSPORT`.
|
|
109
|
+
- **Negotiate and NTLM, for the servers and proxies you name** — a `401` with
|
|
110
|
+
`WWW-Authenticate: Negotiate` or `NTLM`, and a `407` with `Proxy-Authenticate`
|
|
111
|
+
(a `CONNECT` included), is answered leg after leg on the connection it came
|
|
112
|
+
on: SSPI with the current Windows logon, Kerberos from the credential cache
|
|
113
|
+
where `gssapi` is installed, NTLM with a credential you give. Off until
|
|
114
|
+
`SYSHTTP_NEGOTIATE` or `SYSHTTP_PROXY_NEGOTIATE` names a host, and never over
|
|
115
|
+
plain http to a host that is not loopback unless a setting says so
|
|
116
|
+
(`pip install "syshttp[negotiate]"`).
|
|
104
117
|
- **Other programs' clients, patched on import** — `SYSHTTP_IMPORTPATCH=1`
|
|
105
118
|
(or `requests:urllib:httpx:aiohttp:pip`) makes those libraries route and
|
|
106
119
|
verify the same way in any program of the environment, through
|
|
@@ -116,6 +129,8 @@ pip install syshttp
|
|
|
116
129
|
| Extra | Adds | Needed for |
|
|
117
130
|
| --- | --- | --- |
|
|
118
131
|
| `pac` | a JavaScript engine for proxylib | PAC scripts proxylib evaluates itself (Linux without libproxy) |
|
|
132
|
+
| `negotiate` | pyspnego | Negotiate and NTLM for a server or proxy that challenges: SSPI on Windows, NTLM in Python elsewhere |
|
|
133
|
+
| `kerberos` | pyspnego with `gssapi` | Kerberos from the credential cache on Linux and macOS (builds against the system's Kerberos headers when no wheel exists) |
|
|
119
134
|
|
|
120
135
|
syshttp depends on proxylib (the system proxy), sslcontext (the system's
|
|
121
136
|
TLS client: the OS verifier through truststore on Python 3.10+, OpenSSL over
|
|
@@ -152,6 +167,21 @@ with HTTPClient(cookies=True, tokens=True) as client:
|
|
|
152
167
|
response = client.request("GET", "https://api.example.com/thing")
|
|
153
168
|
```
|
|
154
169
|
|
|
170
|
+
A server or proxy behind Windows-integrated authentication, answered with the
|
|
171
|
+
logon session (nothing is sent unless the host is named; needs
|
|
172
|
+
`pip install "syshttp[negotiate]"`):
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from syshttp.client import HTTPClient
|
|
176
|
+
from syshttp.negotiate import Negotiate
|
|
177
|
+
|
|
178
|
+
with HTTPClient(negotiate=Negotiate("wiki.corp.example", proxies="proxy.corp.example")) as client:
|
|
179
|
+
response = client.request("GET", "https://wiki.corp.example/api/page")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The same from the environment, with no code change:
|
|
183
|
+
`SYSHTTP_NEGOTIATE=wiki.corp.example SYSHTTP_PROXY_NEGOTIATE=proxy.corp.example`.
|
|
184
|
+
|
|
155
185
|
A separate configuration, isolated from the process environment:
|
|
156
186
|
|
|
157
187
|
```python
|
|
@@ -168,6 +198,7 @@ response = fetch("https://api.example.com/thing", env=env)
|
|
|
168
198
|
| `syshttp.env` | `HTTPEnv`: the prefixes and mapping every setting is read from |
|
|
169
199
|
| `syshttp.proxy` | `proxy_map()`: the settings as one proxylib proxy map |
|
|
170
200
|
| `syshttp.client` | `HTTPClient` and `fetch()`: the HTTP client, its transport protocol and response |
|
|
201
|
+
| `syshttp.negotiate` | `Negotiate`: which servers and proxies get Negotiate and NTLM, and with whose credentials |
|
|
171
202
|
| `syshttp.jar` | file and memory cookie/token jars |
|
|
172
203
|
| `syshttp.cookies` | Netscape cookie files and curl's cookie rules |
|
|
173
204
|
| `syshttp.hooks` | URL hooks declared in the environment |
|
|
@@ -190,6 +221,11 @@ full list is on the docs site:
|
|
|
190
221
|
| `SYSHTTP_PROXY_AUTH` | the proxy credential, sent verbatim; else `HTTP_PROXY_AUTH` |
|
|
191
222
|
| `SYSHTTP_PROXY_AUTH_HEADER` | the header it is sent in (default `Proxy-Authorization`) |
|
|
192
223
|
| `SYSHTTP_PROXY_TYPE` | `connect` (default) or `prefix[:/endpoint]` |
|
|
224
|
+
| `SYSHTTP_NEGOTIATE` | the servers whose Negotiate or NTLM challenge is answered: host names, `*.suffix`, `*` (default: none) |
|
|
225
|
+
| `SYSHTTP_PROXY_NEGOTIATE` | the same for proxies, by the proxy's host |
|
|
226
|
+
| `SYSHTTP_NEGOTIATE_USER`, `SYSHTTP_NEGOTIATE_PASSWORD` | an explicit identity (a secret) instead of the logon session or credential cache |
|
|
227
|
+
| `SYSHTTP_NEGOTIATE_PLAIN_HTTP` | also authenticate over plain http to a host that is not loopback (off; logged as a weakening) |
|
|
228
|
+
| `SYSHTTP_NEGOTIATE_MAX_LEGS`, `SYSHTTP_NEGOTIATE_MAX_TOKEN` | the limits of one handshake (5 requests, 65536 bytes a token) |
|
|
193
229
|
| `SYSHTTP_HOME` | the jar directory (default: the per-user state directory) |
|
|
194
230
|
| `SYSHTTP_IMPORTPATCH` | `1`, or `:`-separated names: the client libraries patched on import |
|
|
195
231
|
| `SYSHTTP_HOOKS_<KEY>` | a URL hook's pattern, with `_PY` / `_CURL` targets |
|
|
@@ -59,6 +59,14 @@ it. Documentation: <https://jose-pr.github.io/syshttp/>.
|
|
|
59
59
|
all of the above, on `http.client` and `ssl` alone. Its transport is
|
|
60
60
|
replaceable: any object with `send()` (a `requests` or `httpx` one, a fake
|
|
61
61
|
in a test), chosen per client or by `SYSHTTP_DEFAULT_TRANSPORT`.
|
|
62
|
+
- **Negotiate and NTLM, for the servers and proxies you name** — a `401` with
|
|
63
|
+
`WWW-Authenticate: Negotiate` or `NTLM`, and a `407` with `Proxy-Authenticate`
|
|
64
|
+
(a `CONNECT` included), is answered leg after leg on the connection it came
|
|
65
|
+
on: SSPI with the current Windows logon, Kerberos from the credential cache
|
|
66
|
+
where `gssapi` is installed, NTLM with a credential you give. Off until
|
|
67
|
+
`SYSHTTP_NEGOTIATE` or `SYSHTTP_PROXY_NEGOTIATE` names a host, and never over
|
|
68
|
+
plain http to a host that is not loopback unless a setting says so
|
|
69
|
+
(`pip install "syshttp[negotiate]"`).
|
|
62
70
|
- **Other programs' clients, patched on import** — `SYSHTTP_IMPORTPATCH=1`
|
|
63
71
|
(or `requests:urllib:httpx:aiohttp:pip`) makes those libraries route and
|
|
64
72
|
verify the same way in any program of the environment, through
|
|
@@ -74,6 +82,8 @@ pip install syshttp
|
|
|
74
82
|
| Extra | Adds | Needed for |
|
|
75
83
|
| --- | --- | --- |
|
|
76
84
|
| `pac` | a JavaScript engine for proxylib | PAC scripts proxylib evaluates itself (Linux without libproxy) |
|
|
85
|
+
| `negotiate` | pyspnego | Negotiate and NTLM for a server or proxy that challenges: SSPI on Windows, NTLM in Python elsewhere |
|
|
86
|
+
| `kerberos` | pyspnego with `gssapi` | Kerberos from the credential cache on Linux and macOS (builds against the system's Kerberos headers when no wheel exists) |
|
|
77
87
|
|
|
78
88
|
syshttp depends on proxylib (the system proxy), sslcontext (the system's
|
|
79
89
|
TLS client: the OS verifier through truststore on Python 3.10+, OpenSSL over
|
|
@@ -110,6 +120,21 @@ with HTTPClient(cookies=True, tokens=True) as client:
|
|
|
110
120
|
response = client.request("GET", "https://api.example.com/thing")
|
|
111
121
|
```
|
|
112
122
|
|
|
123
|
+
A server or proxy behind Windows-integrated authentication, answered with the
|
|
124
|
+
logon session (nothing is sent unless the host is named; needs
|
|
125
|
+
`pip install "syshttp[negotiate]"`):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from syshttp.client import HTTPClient
|
|
129
|
+
from syshttp.negotiate import Negotiate
|
|
130
|
+
|
|
131
|
+
with HTTPClient(negotiate=Negotiate("wiki.corp.example", proxies="proxy.corp.example")) as client:
|
|
132
|
+
response = client.request("GET", "https://wiki.corp.example/api/page")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The same from the environment, with no code change:
|
|
136
|
+
`SYSHTTP_NEGOTIATE=wiki.corp.example SYSHTTP_PROXY_NEGOTIATE=proxy.corp.example`.
|
|
137
|
+
|
|
113
138
|
A separate configuration, isolated from the process environment:
|
|
114
139
|
|
|
115
140
|
```python
|
|
@@ -126,6 +151,7 @@ response = fetch("https://api.example.com/thing", env=env)
|
|
|
126
151
|
| `syshttp.env` | `HTTPEnv`: the prefixes and mapping every setting is read from |
|
|
127
152
|
| `syshttp.proxy` | `proxy_map()`: the settings as one proxylib proxy map |
|
|
128
153
|
| `syshttp.client` | `HTTPClient` and `fetch()`: the HTTP client, its transport protocol and response |
|
|
154
|
+
| `syshttp.negotiate` | `Negotiate`: which servers and proxies get Negotiate and NTLM, and with whose credentials |
|
|
129
155
|
| `syshttp.jar` | file and memory cookie/token jars |
|
|
130
156
|
| `syshttp.cookies` | Netscape cookie files and curl's cookie rules |
|
|
131
157
|
| `syshttp.hooks` | URL hooks declared in the environment |
|
|
@@ -148,6 +174,11 @@ full list is on the docs site:
|
|
|
148
174
|
| `SYSHTTP_PROXY_AUTH` | the proxy credential, sent verbatim; else `HTTP_PROXY_AUTH` |
|
|
149
175
|
| `SYSHTTP_PROXY_AUTH_HEADER` | the header it is sent in (default `Proxy-Authorization`) |
|
|
150
176
|
| `SYSHTTP_PROXY_TYPE` | `connect` (default) or `prefix[:/endpoint]` |
|
|
177
|
+
| `SYSHTTP_NEGOTIATE` | the servers whose Negotiate or NTLM challenge is answered: host names, `*.suffix`, `*` (default: none) |
|
|
178
|
+
| `SYSHTTP_PROXY_NEGOTIATE` | the same for proxies, by the proxy's host |
|
|
179
|
+
| `SYSHTTP_NEGOTIATE_USER`, `SYSHTTP_NEGOTIATE_PASSWORD` | an explicit identity (a secret) instead of the logon session or credential cache |
|
|
180
|
+
| `SYSHTTP_NEGOTIATE_PLAIN_HTTP` | also authenticate over plain http to a host that is not loopback (off; logged as a weakening) |
|
|
181
|
+
| `SYSHTTP_NEGOTIATE_MAX_LEGS`, `SYSHTTP_NEGOTIATE_MAX_TOKEN` | the limits of one handshake (5 requests, 65536 bytes a token) |
|
|
151
182
|
| `SYSHTTP_HOME` | the jar directory (default: the per-user state directory) |
|
|
152
183
|
| `SYSHTTP_IMPORTPATCH` | `1`, or `:`-separated names: the client libraries patched on import |
|
|
153
184
|
| `SYSHTTP_HOOKS_<KEY>` | a URL hook's pattern, with `_PY` / `_CURL` targets |
|
|
@@ -15,7 +15,7 @@ response = fetch("https://example.com/", cookies=True, tokens=True)
|
|
|
15
15
|
print(response.status, response.headers["Content-Type"])
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
- [Settings](settings.md): the variables, their prefix, and
|
|
18
|
+
- [Settings](settings.md): the variables, their prefix, `HTTPEnv`, and Negotiate and NTLM.
|
|
19
19
|
- API reference: one page per module, in the navigation.
|
|
20
20
|
- [Changelog](changelog.md).
|
|
21
21
|
|
|
@@ -17,6 +17,12 @@ read when they are needed, never at import.
|
|
|
17
17
|
| `SYSHTTP_PROXY_AUTH` | `SYSHTTP_PROXY`'s credential, a header value sent verbatim to it only (a secret) | the URL's userinfo as Basic |
|
|
18
18
|
| `SYSHTTP_PROXY_AUTH_HEADER` | the header `SYSHTTP_PROXY_AUTH` rides in | `Proxy-Authorization` |
|
|
19
19
|
| `SYSHTTP_PROXY_TYPE` | `connect`, or `prefix[:/endpoint]` for a URL-prefix gateway | `connect` |
|
|
20
|
+
| `SYSHTTP_NEGOTIATE` | the servers whose `Negotiate` or `NTLM` challenge is answered: a comma list of host names, `*.suffix` for a domain, `*` for all; needs the `negotiate` extra | none |
|
|
21
|
+
| `SYSHTTP_PROXY_NEGOTIATE` | the same for proxies, by the proxy's host | none |
|
|
22
|
+
| `SYSHTTP_NEGOTIATE_USER`, `SYSHTTP_NEGOTIATE_PASSWORD` | an explicit identity (`DOMAIN\user` or `user@REALM`) for every allowed host; both or neither; the password is a secret | the logon session (SSPI) or the Kerberos credential cache |
|
|
23
|
+
| `SYSHTTP_NEGOTIATE_PLAIN_HTTP` | whether to authenticate over plain http to a host that is not loopback (boolean); each such host is logged as a weakening | off |
|
|
24
|
+
| `SYSHTTP_NEGOTIATE_MAX_LEGS` | most authenticated requests of one handshake | 5 |
|
|
25
|
+
| `SYSHTTP_NEGOTIATE_MAX_TOKEN` | most bytes of one token, sent or received | 65536 |
|
|
20
26
|
| `SYSHTTP_HOME` | the directory of the cookie and token jars | the per-user state directory |
|
|
21
27
|
| `SYSHTTP_COOKIE_HOSTS` | groups of hosts that share cookies: hosts split by `,`, groups by `;` | none |
|
|
22
28
|
| `SYSHTTP_IMPORTPATCH` | client libraries patched as they are imported to use the system proxy and TLS: `1` for all, or `:`-separated names from `requests`, `urllib`, `httpx`, `aiohttp`, `pip` | none |
|
|
@@ -96,6 +102,54 @@ does not send an `https:` origin through a plain `prefix+http` gateway, whose
|
|
|
96
102
|
hop would carry that origin's tokens and Secure cookies in clear text: use an
|
|
97
103
|
`https` gateway.
|
|
98
104
|
|
|
105
|
+
## Negotiate and NTLM
|
|
106
|
+
|
|
107
|
+
A server that answers `401` with `WWW-Authenticate: Negotiate` or `NTLM`, and a
|
|
108
|
+
proxy that answers `407` with `Proxy-Authenticate` (to a `CONNECT` as well), is
|
|
109
|
+
answered by `syshttp.client` only for the hosts the settings name, and only
|
|
110
|
+
with the `negotiate` extra installed:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
pip install "syshttp[negotiate]"
|
|
114
|
+
SYSHTTP_NEGOTIATE="wiki.corp.example,*.apps.corp.example"
|
|
115
|
+
SYSHTTP_PROXY_NEGOTIATE="proxy.corp.example"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A pattern is a host name, `*.suffix` for every host below a domain (not the
|
|
119
|
+
domain itself), or `*`. With neither variable set the library never sends a
|
|
120
|
+
token and a `401` or `407` is the reply, as for any other client.
|
|
121
|
+
|
|
122
|
+
- **Whose identity.** With no `SYSHTTP_NEGOTIATE_USER` and
|
|
123
|
+
`SYSHTTP_NEGOTIATE_PASSWORD`, the logon session on Windows (SSPI) and the
|
|
124
|
+
Kerberos credential cache on Linux and macOS (the `kerberos` extra adds
|
|
125
|
+
`gssapi`; without it pyspnego's own NTLM needs the explicit identity). With
|
|
126
|
+
them, that one identity for every host the lists name. A password is a secret:
|
|
127
|
+
it appears in no `repr()`, log line or message, and belongs in the
|
|
128
|
+
environment or a secret store, never on a command line.
|
|
129
|
+
- **The exchange.** Each leg is the same request again with a token, on the
|
|
130
|
+
connection the challenge came on and with `Connection: keep-alive`, since
|
|
131
|
+
NTLM binds the identity to the connection. The request is not answered when
|
|
132
|
+
it names an `Authorization` (a `Proxy-Authorization` for a proxy), when the
|
|
133
|
+
proxy has a credential of its own, when its body cannot be sent twice, and for
|
|
134
|
+
a prefix gateway. The server's closing token is checked where the mechanism
|
|
135
|
+
has one (Kerberos); NTLM has none.
|
|
136
|
+
- **Plain http.** A token crossing plain http is not tried for a host that is
|
|
137
|
+
not loopback, and the request fails with `NegotiationError` naming
|
|
138
|
+
`SYSHTTP_NEGOTIATE_PLAIN_HTTP`. Setting it to `1` allows it, and each such
|
|
139
|
+
host is logged once as `weakened` under the `syshttp.client` logger. An
|
|
140
|
+
`https:` server is judged by its own scheme; a proxy by the proxy URL's.
|
|
141
|
+
- **Limits and failures.** `SYSHTTP_NEGOTIATE_MAX_LEGS` bounds the
|
|
142
|
+
authenticated requests of one handshake (NTLM takes two) and
|
|
143
|
+
`SYSHTTP_NEGOTIATE_MAX_TOKEN` a token in either direction. Refused
|
|
144
|
+
credentials, a token that is not valid, a closed connection, a limit and a
|
|
145
|
+
failure of the library raise `NegotiationError`, once: nothing is sent again.
|
|
146
|
+
The extra missing raises `NegotiateUnavailableError`, which is also an
|
|
147
|
+
`ImportError`, only for a host the settings name.
|
|
148
|
+
|
|
149
|
+
In code, `HTTPClient(negotiate=Negotiate(...))` (or `HTTPTransport(negotiate=...)`)
|
|
150
|
+
takes the same settings as arguments, which win over the variables; the
|
|
151
|
+
[`syshttp.negotiate` reference](api/negotiate.md) lists them.
|
|
152
|
+
|
|
99
153
|
## The default transport
|
|
100
154
|
|
|
101
155
|
An `HTTPClient` (and so `fetch()`) built without a `transport=` sends through
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "syshttp"
|
|
8
|
-
version = "0.1.
|
|
8
|
+
version = "0.1.1"
|
|
9
9
|
authors = [{ name = "jose-pr", email = "jose-pr@coqui.dev" }]
|
|
10
10
|
description = "HTTP plumbing that follows the system: the system proxy (PAC, WPAD) and the OS certificate verifier by default, proxy credentials and prefix gateways, Netscape cookie jars, OS trust-store TLS, URL hooks, and an HTTP client on the standard library"
|
|
11
11
|
readme = "README.md"
|
|
@@ -51,9 +51,17 @@ aiohttp = "syshttp.autopatch"
|
|
|
51
51
|
# A JavaScript engine for PAC scripts proxylib evaluates itself (Linux without
|
|
52
52
|
# libproxy); Windows and macOS evaluate PAC in the OS.
|
|
53
53
|
pac = ["proxylib[pac]>=2.0.2,<3"]
|
|
54
|
+
# Negotiate and NTLM authentication (syshttp.negotiate): SSPI on Windows with the
|
|
55
|
+
# logon session, GSSAPI where `gssapi` is installed (the `kerberos` extra), and
|
|
56
|
+
# NTLM in Python elsewhere.
|
|
57
|
+
negotiate = ["pyspnego>=0.12.0,<0.13"]
|
|
58
|
+
# Kerberos from the credential cache on Linux and macOS: pyspnego's GSSAPI
|
|
59
|
+
# support, which builds against the system's Kerberos headers (krb5-devel,
|
|
60
|
+
# libkrb5-dev) when no wheel exists.
|
|
61
|
+
kerberos = ["pyspnego[kerberos]>=0.12.0,<0.13"]
|
|
54
62
|
# requests, httpx and aiohttp are what syshttp.autopatch patches: the tests
|
|
55
63
|
# use them. syshttp's own client needs none of them.
|
|
56
|
-
dev = ["requests", "httpx", "aiohttp", "build", "pytest", "black", "mypy<2", "types-requests"]
|
|
64
|
+
dev = ["requests", "httpx", "aiohttp", "build", "pytest", "black", "mypy<2", "types-requests", "syshttp[negotiate]"]
|
|
57
65
|
docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]"]
|
|
58
66
|
|
|
59
67
|
[project.urls]
|
|
@@ -80,6 +88,12 @@ line-length = 120
|
|
|
80
88
|
[tool.pytest.ini_options]
|
|
81
89
|
pythonpath = ["src"]
|
|
82
90
|
testpaths = ["tests"]
|
|
83
|
-
# Warnings are errors. An exception
|
|
84
|
-
#
|
|
85
|
-
filterwarnings = [
|
|
91
|
+
# Warnings are errors. An exception is listed here as narrowly as the warning's
|
|
92
|
+
# own text, with the reason it is tolerated.
|
|
93
|
+
filterwarnings = [
|
|
94
|
+
"error",
|
|
95
|
+
# cryptography, the cipher library of pyspnego, says so at import when its
|
|
96
|
+
# OpenSSL has no legacy provider (a Windows ARM64 Python ships none). NTLM
|
|
97
|
+
# needs RC4, which lives there: the tests that need it probe for it and skip.
|
|
98
|
+
"ignore:OpenSSL 3's legacy provider failed to load:Warning",
|
|
99
|
+
]
|
|
@@ -7,7 +7,7 @@ self-contained. Development documentation lives with the source at
|
|
|
7
7
|
<https://github.com/jose-pr/syshttp>.
|
|
8
8
|
|
|
9
9
|
Import names from their modules: `syshttp.env`, `syshttp.proxy`,
|
|
10
|
-
`syshttp.client`, `syshttp.jar`, `syshttp.cookies`, `syshttp.hooks`,
|
|
10
|
+
`syshttp.client`, `syshttp.negotiate`, `syshttp.jar`, `syshttp.cookies`, `syshttp.hooks`,
|
|
11
11
|
`syshttp.tls`, `syshttp.retry`, `syshttp.auth`, `syshttp.exceptions`,
|
|
12
12
|
`syshttp.autopatch`. Each declares `__all__`: a name outside it is not API,
|
|
13
13
|
and a module whose name starts with `_` is private. The root re-exports
|
|
@@ -19,9 +19,11 @@ module.
|
|
|
19
19
|
`pip install syshttp` brings proxylib 2.0.2+ (proxy routing and the system's proxy configuration),
|
|
20
20
|
sslcontext (the system's TLS client; truststore on Python 3.10+) and
|
|
21
21
|
importpatch (import-time patching of other HTTP clients, off unless
|
|
22
|
-
`SYSHTTP_IMPORTPATCH` asks for it). The
|
|
22
|
+
`SYSHTTP_IMPORTPATCH` asks for it). The extras: `pac`
|
|
23
23
|
adds a JavaScript engine for PAC scripts proxylib evaluates itself (Linux
|
|
24
|
-
without libproxy)
|
|
24
|
+
without libproxy); `negotiate` adds pyspnego for Negotiate and NTLM
|
|
25
|
+
(`syshttp.negotiate`); `kerberos` adds `gssapi` to it for the Kerberos
|
|
26
|
+
credential cache on Linux and macOS. The client needs nothing else: `requests` is no dependency
|
|
25
27
|
of any feature; only `syshttp.autopatch` touches it, in a program that imports
|
|
26
28
|
it, when `SYSHTTP_IMPORTPATCH` asks for it.
|
|
27
29
|
|
|
@@ -175,7 +177,8 @@ class HTTPClient:
|
|
|
175
177
|
cookies: bool | str | CookieJar | None = False, tokens: bool | str | TokenJar | None = False,
|
|
176
178
|
auth_provider: AuthProvider | None = None, retries: int = 3, backoff: float = 1,
|
|
177
179
|
status_forcelist: Iterable[int] = TRANSIENT_STATUSES,
|
|
178
|
-
cookie_hosts: Iterable[Iterable[str]] | None = None
|
|
180
|
+
cookie_hosts: Iterable[Iterable[str]] | None = None,
|
|
181
|
+
negotiate: Negotiate | None = None)
|
|
179
182
|
transport # the transport in use
|
|
180
183
|
def request(self, method, url, *, data=None, headers=None, stream=False, allow_redirects=None,
|
|
181
184
|
timeout=..., deadline=..., auth=None) -> HTTPResponse
|
|
@@ -319,6 +322,11 @@ DEFAULT_TRANSPORT = "SYSHTTP_DEFAULT_TRANSPORT" # setting names
|
|
|
319
322
|
is used instead of sending. A request a provider or a hook makes through the
|
|
320
323
|
client runs neither again; the guard is per thread, so another thread's
|
|
321
324
|
requests still run them.
|
|
325
|
+
- **Negotiate and NTLM.** `negotiate=` (a `syshttp.negotiate.Negotiate`, see
|
|
326
|
+
its section) configures the default transport as `proxymap` does: a transport
|
|
327
|
+
of your own, or `SYSHTTP_DEFAULT_TRANSPORT`, beside it is refused. Without
|
|
328
|
+
it the transport reads `SYSHTTP_NEGOTIATE` and its kin, which name no host by
|
|
329
|
+
default.
|
|
322
330
|
- **Retries.** A status in `status_forcelist` (default `TRANSIENT_STATUSES`),
|
|
323
331
|
or a failed connection (`ConnectionError`, not a timeout, not
|
|
324
332
|
`NoReachableProxyError`), is sent again up to `retries` times (default 3) for
|
|
@@ -433,7 +441,8 @@ class PreparedRequest(NamedTuple):
|
|
|
433
441
|
def prepare_request(method, url, *, headers, body=None, timeout) -> PreparedRequest
|
|
434
442
|
|
|
435
443
|
class HTTPTransport:
|
|
436
|
-
def __init__(self, *, proxymap=None, env=None, ssl_context=None, proxy_ssl_context=None, verify=True
|
|
444
|
+
def __init__(self, *, proxymap=None, env=None, ssl_context=None, proxy_ssl_context=None, verify=True,
|
|
445
|
+
negotiate: Negotiate | None = None)
|
|
437
446
|
def send(self, method, url, *, headers, body=None, timeout, stream=False) -> HTTPResponse
|
|
438
447
|
def close(self) -> None
|
|
439
448
|
```
|
|
@@ -504,7 +513,102 @@ process using syshttp.
|
|
|
504
513
|
unless it implements them; it can still be used by `fetch(transport=...)`.
|
|
505
514
|
`HTTPTransport` adds to the contract's exceptions
|
|
506
515
|
`proxylib.NoReachableProxyError`, `proxylib.UnsupportedProxyError` and
|
|
507
|
-
`ConfigurationError
|
|
516
|
+
`ConfigurationError`, and `NegotiationError` or `NegotiateUnavailableError`
|
|
517
|
+
for a server or proxy the `negotiate` policy names.
|
|
518
|
+
|
|
519
|
+
## Negotiate and NTLM (`syshttp.negotiate`)
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
class Negotiate:
|
|
523
|
+
def __init__(self, hosts: str | Iterable[str] | None = None, proxies: str | Iterable[str] | None = None, *,
|
|
524
|
+
username: str | None = None, password: str | None = None, plain_http: bool | None = None,
|
|
525
|
+
max_legs: int | None = None, max_token: int | None = None, env: HTTPEnv | None = None) -> None
|
|
526
|
+
def settings(self) -> NegotiateSettings
|
|
527
|
+
def wants(self, host: str, *, proxy: bool = False) -> bool
|
|
528
|
+
|
|
529
|
+
@dataclass(frozen=True)
|
|
530
|
+
class NegotiateSettings: hosts: tuple[str, ...]; proxies: tuple[str, ...]; plain_http: bool; max_legs: int; max_token: int; explicit_credentials: bool
|
|
531
|
+
|
|
532
|
+
def has_negotiate() -> bool
|
|
533
|
+
DEFAULT_MAX_LEGS = 5; DEFAULT_MAX_TOKEN = 65536
|
|
534
|
+
NEGOTIATE = "SYSHTTP_NEGOTIATE"; PROXY_NEGOTIATE = "SYSHTTP_PROXY_NEGOTIATE"
|
|
535
|
+
NEGOTIATE_USER = "SYSHTTP_NEGOTIATE_USER"; NEGOTIATE_PASSWORD = "SYSHTTP_NEGOTIATE_PASSWORD"
|
|
536
|
+
NEGOTIATE_PLAIN_HTTP = "SYSHTTP_NEGOTIATE_PLAIN_HTTP"
|
|
537
|
+
NEGOTIATE_MAX_LEGS = "SYSHTTP_NEGOTIATE_MAX_LEGS"; NEGOTIATE_MAX_TOKEN = "SYSHTTP_NEGOTIATE_MAX_TOKEN" # setting names
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
`pip install "syshttp[negotiate]"` (pyspnego) is the one dependency; `kerberos`
|
|
541
|
+
adds `gssapi` for the credential cache on Linux and macOS. `import syshttp`,
|
|
542
|
+
`syshttp.negotiate` and `syshttp.client` import neither; pyspnego is imported
|
|
543
|
+
at the first handshake.
|
|
544
|
+
|
|
545
|
+
- **Off by default.** `HTTPTransport(negotiate=...)` and
|
|
546
|
+
`HTTPClient(negotiate=...)` take a `Negotiate`; with none given the transport
|
|
547
|
+
uses `Negotiate(env=env)`. A `401` with `WWW-Authenticate: Negotiate` or `NTLM`
|
|
548
|
+
and a `407` with `Proxy-Authenticate` are answered only for a host the policy
|
|
549
|
+
names; any other is the reply, as for any client. `hosts` (else
|
|
550
|
+
`SYSHTTP_NEGOTIATE`) names servers and `proxies` (else `SYSHTTP_PROXY_NEGOTIATE`)
|
|
551
|
+
proxies, by the host of the proxy: a comma list or an iterable of host names,
|
|
552
|
+
`*.suffix` for every host below a domain (not the domain), `*` for all; an
|
|
553
|
+
empty list is none and beats the variable. A host name is compared without
|
|
554
|
+
case, brackets or a trailing dot.
|
|
555
|
+
- **The exchange.** The transport sends the same request again with `Authorization:
|
|
556
|
+
<Negotiate|NTLM> <base64 token>` (`Proxy-Authorization` for a proxy) on the
|
|
557
|
+
connection the challenge came on, with `Connection: keep-alive`, until the reply
|
|
558
|
+
is not the challenge again; `Negotiate` is preferred when a reply offers
|
|
559
|
+
both. A `CONNECT` the proxy answers `407` is answered the same way, on the
|
|
560
|
+
proxy connection, before the tunnel is made; a proxy's challenge is
|
|
561
|
+
answered before a server's, each once. The body of a challenge is read (up to
|
|
562
|
+
64 KiB) and dropped; the reply that follows is what `send()` returns, and
|
|
563
|
+
the connection, now authenticated, goes back to the pool. The server's
|
|
564
|
+
closing token is taken from that reply when the mechanism proves the server
|
|
565
|
+
(Kerberos): a reply of 400 or more is returned unchecked; an incomplete
|
|
566
|
+
exchange with no closing token, or one the token does not complete, is
|
|
567
|
+
`NegotiationError`; NTLM has none.
|
|
568
|
+
- **Not answered** (the reply is returned): the request names an `Authorization`
|
|
569
|
+
(a `Proxy-Authorization` for a proxy challenge), the proxy has a credential of
|
|
570
|
+
its own (userinfo, headers), the body cannot be sent twice (a file, an
|
|
571
|
+
iterable), the route is a prefix gateway, or the reply offers neither scheme. A
|
|
572
|
+
plain-http server behind a proxy that also challenges is answered after the
|
|
573
|
+
proxy, but the proxy's token is not repeated on the server's legs: a
|
|
574
|
+
Kerberos proxy asks again and its `407` is returned. A transport of your own
|
|
575
|
+
does its own authentication.
|
|
576
|
+
- **Identity.** `username` and `password` (else `SYSHTTP_NEGOTIATE_USER` and
|
|
577
|
+
`SYSHTTP_NEGOTIATE_PASSWORD`, both or `ConfigurationError`) are one identity
|
|
578
|
+
for every host the policy names, as `DOMAIN\user` or `user@REALM`; with none,
|
|
579
|
+
the Windows logon session (SSPI) or the Kerberos credential cache. The
|
|
580
|
+
password is a secret held by the object and in no `repr()`, message, log
|
|
581
|
+
line or exception attribute; `repr(Negotiate)` says `credentials=explicit`
|
|
582
|
+
or `logon`, `NegotiateSettings` holds `explicit_credentials`, never the
|
|
583
|
+
identity. A caller's bad argument is `ValueError` or `TypeError` (a half
|
|
584
|
+
identity, a line break in either part, `max_legs` below 1, a host that is not a
|
|
585
|
+
name or pattern); a variable that cannot be used is `ConfigurationError`
|
|
586
|
+
naming it, refused when the transport is made.
|
|
587
|
+
- **Plain http.** A handshake over `http:` (the server's scheme; for a proxy
|
|
588
|
+
challenge the proxy URL's) to a host that is not `localhost`, `127.0.0.0/8` or
|
|
589
|
+
`::1` is `NegotiationError` naming `SYSHTTP_NEGOTIATE_PLAIN_HTTP`, before any
|
|
590
|
+
token is made. `plain_http=True` (else the variable, a boolean) allows it;
|
|
591
|
+
each such server or proxy is then logged once per transport at WARNING, under
|
|
592
|
+
`syshttp.client`, as `weakened`. An `https:` server through a proxy is judged
|
|
593
|
+
by its own scheme.
|
|
594
|
+
- **Limits.** `max_legs` (default 5, `SYSHTTP_NEGOTIATE_MAX_LEGS`) is the most
|
|
595
|
+
authenticated requests of one handshake: NTLM takes two, Kerberos one or two.
|
|
596
|
+
`max_token` (default 65536, `SYSHTTP_NEGOTIATE_MAX_TOKEN`) is the most
|
|
597
|
+
bytes of a token, sent or received. Past either: `NegotiationError`, and
|
|
598
|
+
nothing more is sent.
|
|
599
|
+
- **Failures.** `NegotiationError` (message: the host and port, the
|
|
600
|
+
mechanism, never a token or a credential): the credentials refused (the
|
|
601
|
+
challenge again), a challenge answered with a token that is not valid
|
|
602
|
+
base64, a server that closes the connection or sends more than 64 KiB between
|
|
603
|
+
legs, a limit, the library's own failure (`__cause__`), a refusal over plain
|
|
604
|
+
http. `NegotiateUnavailableError` (also `ImportError`): pyspnego is
|
|
605
|
+
missing or does not import, naming `pip install "syshttp[negotiate]"`; raised
|
|
606
|
+
only for a host the policy names. A handshake is never sent again by
|
|
607
|
+
syshttp: `NegotiationError` is not a `ConnectionError`, so the client's retries
|
|
608
|
+
do not repeat it.
|
|
609
|
+
- **Channel binding** (Extended Protection for Authentication) is not sent: a
|
|
610
|
+
server that requires it refuses the exchange. `has_negotiate()` is whether
|
|
611
|
+
pyspnego imports.
|
|
508
612
|
|
|
509
613
|
## Exceptions (`syshttp.exceptions`)
|
|
510
614
|
|
|
@@ -513,6 +617,8 @@ class SysHTTPError(Exception)
|
|
|
513
617
|
class ConfigurationError(SysHTTPError, ValueError)
|
|
514
618
|
class HookError(SysHTTPError, RuntimeError)
|
|
515
619
|
class ResponseError(SysHTTPError, ValueError)
|
|
620
|
+
class NegotiationError(SysHTTPError)
|
|
621
|
+
class NegotiateUnavailableError(NegotiationError, ImportError)
|
|
516
622
|
```
|
|
517
623
|
|
|
518
624
|
- `SysHTTPError` is the base of what goes wrong at run time. proxylib's own
|
|
@@ -524,6 +630,10 @@ class ResponseError(SysHTTPError, ValueError)
|
|
|
524
630
|
`SYSHTTP_PREFIX`). The message names the variable or key, never a secret.
|
|
525
631
|
- `HookError`: a `_PY` hook failed; the hook's own exception is `__cause__`.
|
|
526
632
|
- `ResponseError`: a body that is not the JSON `HTTPResponse.json()` was asked for.
|
|
633
|
+
- `NegotiationError`: a Negotiate or NTLM handshake that did not authenticate
|
|
634
|
+
(see "Negotiate and NTLM"); `NegotiateUnavailableError` is the one raised when
|
|
635
|
+
pyspnego is missing or does not import, naming the `negotiate` extra. The
|
|
636
|
+
message names the host, never a token or a credential.
|
|
527
637
|
- A caller's own mistake is a plain `ValueError` or `TypeError`, not one of
|
|
528
638
|
these: a malformed `prefixes=` argument, `verify=False` beside `ssl_context`,
|
|
529
639
|
a `client` beside `client_options` in `fetch()`, an unknown patch target.
|
|
@@ -776,6 +886,13 @@ def patched() -> tuple[str, ...]
|
|
|
776
886
|
| `HTTP_PROXY_AUTH` | **secret**: the machine's proxy's credential (a `*_PROXY` variable's or the system's), as `Proxy-Authorization` | the URL's userinfo as Basic |
|
|
777
887
|
| `SYSHTTP_PROXY_AUTH_HEADER` | the header name `SYSHTTP_PROXY_AUTH` rides in | `Proxy-Authorization` |
|
|
778
888
|
| `SYSHTTP_PROXY_TYPE` | `connect` or `prefix[:/endpoint]` | `connect` |
|
|
889
|
+
| `SYSHTTP_NEGOTIATE` | the servers whose Negotiate or NTLM challenge is answered: comma list of host names, `*.suffix`, `*` | none |
|
|
890
|
+
| `SYSHTTP_PROXY_NEGOTIATE` | the same for proxies, by the proxy's host | none |
|
|
891
|
+
| `SYSHTTP_NEGOTIATE_USER` | the explicit identity's name, `DOMAIN\user` or `user@REALM` (with `_PASSWORD`) | the logon session or credential cache |
|
|
892
|
+
| `SYSHTTP_NEGOTIATE_PASSWORD` | **secret**: the explicit identity's password | none |
|
|
893
|
+
| `SYSHTTP_NEGOTIATE_PLAIN_HTTP` | boolean: authenticate over plain http to a host that is not loopback (logged as a weakening) | off |
|
|
894
|
+
| `SYSHTTP_NEGOTIATE_MAX_LEGS` | most authenticated requests of one handshake | 5 |
|
|
895
|
+
| `SYSHTTP_NEGOTIATE_MAX_TOKEN` | most bytes of one token, sent or received | 65536 |
|
|
779
896
|
| `SYSHTTP_HOME` | the jar root | the per-user state directory |
|
|
780
897
|
| `SYSHTTP_COOKIE_HOSTS` | groups of hosts that share cookies: hosts split by `,`, groups by `;` (`HTTPClient`) | none |
|
|
781
898
|
| `SYSHTTP_DEFAULT_TRANSPORT` | the transport of an `HTTPClient` (and `fetch()`) given none: `module:attr` or `<file>.py:attr`, an object with `send()` or something that makes one. It names code to import | syshttp's `HTTPTransport` |
|
|
@@ -815,3 +932,15 @@ proxylib's own `PROXYLIB_IMPORTPATCH` and `PROXYLIB_AUTOPROXY`.
|
|
|
815
932
|
an untrusted party set the environment of a process using syshttp.
|
|
816
933
|
- A client keeps the cookies its responses set for its whole life, as a
|
|
817
934
|
session does: build one per identity, or pass `cookies=` a jar of its own.
|
|
935
|
+
- Negotiate and NTLM send nothing until `SYSHTTP_NEGOTIATE` or
|
|
936
|
+
`SYSHTTP_PROXY_NEGOTIATE` (or a `Negotiate(hosts=...)`) names the host: the
|
|
937
|
+
logon identity is released only to hosts you list, never to whatever
|
|
938
|
+
challenges. Over plain http, to anything but loopback, it needs
|
|
939
|
+
`SYSHTTP_NEGOTIATE_PLAIN_HTTP` as well.
|
|
940
|
+
- In a venv of the Windows ARM64 CPython 3.9 and 3.14 tested (cryptography
|
|
941
|
+
50.0.2, pyspnego 0.12.4), `import spnego` fails with "DLL load failed" until
|
|
942
|
+
the interpreter's `DLLs` directory is on the DLL search path
|
|
943
|
+
(`os.add_dll_directory`), and then warns that OpenSSL's legacy provider did
|
|
944
|
+
not load, which leaves pyspnego's own NTLM without RC4; SSPI, once the import
|
|
945
|
+
works, does not need it. `has_negotiate()` is the probe, and a handshake that
|
|
946
|
+
needs what is missing is `NegotiationError` or `NegotiateUnavailableError`.
|
|
@@ -10,6 +10,8 @@ Public surface (the package's AGENTS.md documents each name):
|
|
|
10
10
|
--noproxy overrides included)
|
|
11
11
|
proxy.configured_proxy / proxy_type — SYSHTTP_PROXY as a proxylib Proxy; SYSHTTP_PROXY_TYPE
|
|
12
12
|
(connect | prefix[:/endpoint])
|
|
13
|
+
negotiate.Negotiate — Negotiate and NTLM for the servers and proxies SYSHTTP_NEGOTIATE /
|
|
14
|
+
SYSHTTP_PROXY_NEGOTIATE name (the `negotiate` extra)
|
|
13
15
|
client.HTTPClient / fetch — the client: proxy map, system TLS, cookies, tokens,
|
|
14
16
|
hooks, retries; replaceable transport. ``fetch`` is
|
|
15
17
|
also ``syshttp.fetch``
|
|
@@ -32,12 +34,21 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
32
34
|
from typing import Any
|
|
33
35
|
|
|
34
36
|
from .env import HTTPEnv
|
|
35
|
-
from .exceptions import
|
|
37
|
+
from .exceptions import (
|
|
38
|
+
ConfigurationError,
|
|
39
|
+
HookError,
|
|
40
|
+
NegotiateUnavailableError,
|
|
41
|
+
NegotiationError,
|
|
42
|
+
ResponseError,
|
|
43
|
+
SysHTTPError,
|
|
44
|
+
)
|
|
36
45
|
|
|
37
46
|
__all__ = [
|
|
38
47
|
"ConfigurationError",
|
|
39
48
|
"HTTPEnv",
|
|
40
49
|
"HookError",
|
|
50
|
+
"NegotiateUnavailableError",
|
|
51
|
+
"NegotiationError",
|
|
41
52
|
"NoReachableProxyError",
|
|
42
53
|
"ResponseError",
|
|
43
54
|
"SysHTTPError",
|
|
@@ -14,6 +14,7 @@ from .. import __version__
|
|
|
14
14
|
from .. import hooks as _hooks
|
|
15
15
|
from ..env import HTTPEnv
|
|
16
16
|
from ..jar import CookieJar, TokenJar
|
|
17
|
+
from ..negotiate import Negotiate
|
|
17
18
|
from ..retry import TRANSIENT_STATUSES, retry_after
|
|
18
19
|
from . import _follow, _settings
|
|
19
20
|
from ._deadline import Deadline, check_deadline
|
|
@@ -123,6 +124,7 @@ class HTTPClient:
|
|
|
123
124
|
backoff: float = 1,
|
|
124
125
|
status_forcelist: "_ty.Iterable[int]" = TRANSIENT_STATUSES,
|
|
125
126
|
cookie_hosts: "_ty.Iterable[_ty.Iterable[str]] | None" = None,
|
|
127
|
+
negotiate: "Negotiate | None" = None,
|
|
126
128
|
):
|
|
127
129
|
if transport is not None and not callable(getattr(transport, "send", None)):
|
|
128
130
|
raise TypeError("a transport is an object with a send() method")
|
|
@@ -136,6 +138,7 @@ class HTTPClient:
|
|
|
136
138
|
"ssl_context": ssl_context,
|
|
137
139
|
"proxy_ssl_context": proxy_ssl_context,
|
|
138
140
|
"verify": verify,
|
|
141
|
+
"negotiate": negotiate,
|
|
139
142
|
}
|
|
140
143
|
self._transport = transport
|
|
141
144
|
self._built = None
|
|
@@ -143,7 +146,7 @@ class HTTPClient:
|
|
|
143
146
|
if transport is not None:
|
|
144
147
|
if any(value is not (True if name == "verify" else None) for name, value in options.items()):
|
|
145
148
|
raise ValueError(
|
|
146
|
-
"proxymap, ssl_context, proxy_ssl_context and
|
|
149
|
+
"proxymap, ssl_context, proxy_ssl_context, verify and negotiate configure the default transport: "
|
|
147
150
|
"a client given a transport leaves them to it"
|
|
148
151
|
)
|
|
149
152
|
else:
|