wildburrow 0.1.0__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.
- wildburrow-0.1.0/.gitignore +13 -0
- wildburrow-0.1.0/LICENSE +21 -0
- wildburrow-0.1.0/PKG-INFO +231 -0
- wildburrow-0.1.0/README.md +196 -0
- wildburrow-0.1.0/pyproject.toml +129 -0
- wildburrow-0.1.0/src/wildburrow/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/__main__.py +6 -0
- wildburrow-0.1.0/src/wildburrow/auth/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/auth/base.py +24 -0
- wildburrow-0.1.0/src/wildburrow/auth/keys.py +60 -0
- wildburrow-0.1.0/src/wildburrow/auth/password.py +66 -0
- wildburrow-0.1.0/src/wildburrow/auth/sshsig.py +68 -0
- wildburrow-0.1.0/src/wildburrow/auth/token.py +46 -0
- wildburrow-0.1.0/src/wildburrow/certs/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/certs/acme.py +118 -0
- wildburrow-0.1.0/src/wildburrow/certs/manager.py +119 -0
- wildburrow-0.1.0/src/wildburrow/certs/store.py +56 -0
- wildburrow-0.1.0/src/wildburrow/cli/__init__.py +54 -0
- wildburrow-0.1.0/src/wildburrow/cli/ask.py +59 -0
- wildburrow-0.1.0/src/wildburrow/cli/client.py +181 -0
- wildburrow-0.1.0/src/wildburrow/cli/errors.py +12 -0
- wildburrow-0.1.0/src/wildburrow/cli/server.py +182 -0
- wildburrow-0.1.0/src/wildburrow/cli/service.py +310 -0
- wildburrow-0.1.0/src/wildburrow/client/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/client/config.py +60 -0
- wildburrow-0.1.0/src/wildburrow/client/retry.py +19 -0
- wildburrow-0.1.0/src/wildburrow/client/ssh.py +75 -0
- wildburrow-0.1.0/src/wildburrow/client/websocket.py +173 -0
- wildburrow-0.1.0/src/wildburrow/config.py +100 -0
- wildburrow-0.1.0/src/wildburrow/core/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/core/context.py +50 -0
- wildburrow-0.1.0/src/wildburrow/core/errors.py +6 -0
- wildburrow-0.1.0/src/wildburrow/core/names.py +55 -0
- wildburrow-0.1.0/src/wildburrow/core/registry.py +32 -0
- wildburrow-0.1.0/src/wildburrow/core/types.py +68 -0
- wildburrow-0.1.0/src/wildburrow/dns/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/dns/base.py +12 -0
- wildburrow-0.1.0/src/wildburrow/dns/cloudflare.py +72 -0
- wildburrow-0.1.0/src/wildburrow/edge/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/edge/http.py +179 -0
- wildburrow-0.1.0/src/wildburrow/edge/proxy.py +88 -0
- wildburrow-0.1.0/src/wildburrow/envfile.py +63 -0
- wildburrow-0.1.0/src/wildburrow/plugins.py +32 -0
- wildburrow-0.1.0/src/wildburrow/protocol.py +74 -0
- wildburrow-0.1.0/src/wildburrow/server.py +126 -0
- wildburrow-0.1.0/src/wildburrow/service/__init__.py +140 -0
- wildburrow-0.1.0/src/wildburrow/service/launchd.py +81 -0
- wildburrow-0.1.0/src/wildburrow/service/systemd.py +80 -0
- wildburrow-0.1.0/src/wildburrow/streams.py +28 -0
- wildburrow-0.1.0/src/wildburrow/transports/__init__.py +0 -0
- wildburrow-0.1.0/src/wildburrow/transports/ssh.py +197 -0
- wildburrow-0.1.0/src/wildburrow/transports/websocket.py +226 -0
- wildburrow-0.1.0/tests/__init__.py +0 -0
- wildburrow-0.1.0/tests/acme_env.py +101 -0
- wildburrow-0.1.0/tests/conftest.py +278 -0
- wildburrow-0.1.0/tests/e2e/__init__.py +0 -0
- wildburrow-0.1.0/tests/e2e/test_passthrough.py +180 -0
- wildburrow-0.1.0/tests/e2e/test_reconnect.py +113 -0
- wildburrow-0.1.0/tests/e2e/test_tls.py +258 -0
- wildburrow-0.1.0/tests/e2e/test_tunnels.py +405 -0
- wildburrow-0.1.0/tests/e2e/test_websocket.py +599 -0
- wildburrow-0.1.0/tests/plugins/__init__.py +0 -0
- wildburrow-0.1.0/tests/plugins/challtestsrv.py +37 -0
- wildburrow-0.1.0/tests/system/__init__.py +0 -0
- wildburrow-0.1.0/tests/system/test_service.py +89 -0
- wildburrow-0.1.0/tests/unit/__init__.py +0 -0
- wildburrow-0.1.0/tests/unit/conftest.py +78 -0
- wildburrow-0.1.0/tests/unit/help/connect.txt +18 -0
- wildburrow-0.1.0/tests/unit/help/hash-password.txt +6 -0
- wildburrow-0.1.0/tests/unit/help/login.txt +17 -0
- wildburrow-0.1.0/tests/unit/help/logout.txt +7 -0
- wildburrow-0.1.0/tests/unit/help/serve.txt +81 -0
- wildburrow-0.1.0/tests/unit/help/service-edit.txt +78 -0
- wildburrow-0.1.0/tests/unit/help/service-install.txt +79 -0
- wildburrow-0.1.0/tests/unit/help/service-status.txt +6 -0
- wildburrow-0.1.0/tests/unit/help/service-uninstall.txt +8 -0
- wildburrow-0.1.0/tests/unit/help/service.txt +12 -0
- wildburrow-0.1.0/tests/unit/help/token.txt +6 -0
- wildburrow-0.1.0/tests/unit/help/wildburrow.txt +16 -0
- wildburrow-0.1.0/tests/unit/test_ask.py +85 -0
- wildburrow-0.1.0/tests/unit/test_auth.py +188 -0
- wildburrow-0.1.0/tests/unit/test_certs.py +403 -0
- wildburrow-0.1.0/tests/unit/test_cli.py +590 -0
- wildburrow-0.1.0/tests/unit/test_cli_service.py +618 -0
- wildburrow-0.1.0/tests/unit/test_client_ssh.py +112 -0
- wildburrow-0.1.0/tests/unit/test_client_websocket.py +475 -0
- wildburrow-0.1.0/tests/unit/test_cloudflare.py +135 -0
- wildburrow-0.1.0/tests/unit/test_config.py +107 -0
- wildburrow-0.1.0/tests/unit/test_edge.py +132 -0
- wildburrow-0.1.0/tests/unit/test_envfile.py +102 -0
- wildburrow-0.1.0/tests/unit/test_http.py +144 -0
- wildburrow-0.1.0/tests/unit/test_mutation.py +434 -0
- wildburrow-0.1.0/tests/unit/test_names.py +90 -0
- wildburrow-0.1.0/tests/unit/test_plugins.py +39 -0
- wildburrow-0.1.0/tests/unit/test_proxy.py +98 -0
- wildburrow-0.1.0/tests/unit/test_registry.py +97 -0
- wildburrow-0.1.0/tests/unit/test_server.py +213 -0
- wildburrow-0.1.0/tests/unit/test_service.py +400 -0
- wildburrow-0.1.0/tests/unit/test_sshsig.py +144 -0
- wildburrow-0.1.0/tests/unit/test_token.py +44 -0
wildburrow-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Işık Kaplan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wildburrow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-hosted HTTP tunnels with on-demand wildcard certificates at any depth
|
|
5
|
+
Project-URL: Repository, https://github.com/isik-kaplan/wildburrow
|
|
6
|
+
Project-URL: Issues, https://github.com/isik-kaplan/wildburrow/issues
|
|
7
|
+
Author: Işık Kaplan
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: acme,letsencrypt,ngrok,reverse-proxy,ssh,tunnel,websocket,wildcard
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Requires-Python: >=3.14.4
|
|
24
|
+
Requires-Dist: click==8.5.0
|
|
25
|
+
Requires-Dist: questionary==2.1.1
|
|
26
|
+
Requires-Dist: websockets==17.2
|
|
27
|
+
Provides-Extra: server
|
|
28
|
+
Requires-Dist: acme==5.8.0; extra == 'server'
|
|
29
|
+
Requires-Dist: argon2-cffi==25.1.0; extra == 'server'
|
|
30
|
+
Requires-Dist: asyncssh==2.24.1; extra == 'server'
|
|
31
|
+
Requires-Dist: cryptography==50.0.2; extra == 'server'
|
|
32
|
+
Requires-Dist: httpx==0.28.1; extra == 'server'
|
|
33
|
+
Requires-Dist: josepy==2.2.0; extra == 'server'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# wildburrow
|
|
37
|
+
|
|
38
|
+
Self-hosted HTTPS tunnels on your own domain, with wildcard names at any depth and certificates issued on demand.
|
|
39
|
+
It runs on Linux and macOS.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
wildburrow 3000 myapp # https://myapp.tunnel.example.com
|
|
43
|
+
wildburrow 3000 '*.foo' # https://<anything>.foo.tunnel.example.com
|
|
44
|
+
wildburrow 3000 '*.bar.foo' # https://<anything>.bar.foo.tunnel.example.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A wildcard covers exactly one label, like a TLS wildcard certificate: `*.foo` serves `api.foo` but not `x.api.foo`.
|
|
48
|
+
An exact name wins over a wildcard. Quote `*` names so your shell doesn't expand them.
|
|
49
|
+
|
|
50
|
+
Requests reach your local app unchanged, so websockets, server-sent events and streaming responses work as they do
|
|
51
|
+
locally. With HTTPS, visitors use `wss://` and your app sees plain `ws://`.
|
|
52
|
+
|
|
53
|
+
When a name needs a certificate that doesn't exist yet, the server gets `*.<parent>` from an ACME CA through a DNS-01
|
|
54
|
+
challenge while your client waits, then confirms the bind. Later binds under the same parent are instant.
|
|
55
|
+
|
|
56
|
+
## Client
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
uv tool install wildburrow # or: pipx install wildburrow
|
|
60
|
+
wildburrow login # asks for the server and how to connect to it
|
|
61
|
+
wildburrow 3000 '*.foo'
|
|
62
|
+
wildburrow logout # forgets the saved server
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`login` keeps its answers in `~/.config/wildburrow/config.toml` until you log in again or out; nothing in it expires,
|
|
66
|
+
and every tunnel logs in afresh. Given options, `wildburrow login tunnel.example.com --transport websocket`, it asks
|
|
67
|
+
nothing and saves what you passed.
|
|
68
|
+
|
|
69
|
+
Two transports are available:
|
|
70
|
+
|
|
71
|
+
| Transport | How it connects | Auth methods |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `ssh` (default) | your `ssh` binary, port 2222, one `-R` per name | `key`, `password` |
|
|
74
|
+
| `websocket` | `wss://tunnel.example.com` on 443 | `key` (signed with `ssh-keygen -Y sign`), `password`, `token` |
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
wildburrow login tunnel.example.com --transport websocket --auth key --identity ~/.ssh/id_ed25519
|
|
78
|
+
wildburrow login tunnel.example.com --transport websocket --auth token --token wb_...
|
|
79
|
+
WILDBURROW_TOKEN=wb_... wildburrow 3000 app # token from the environment
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The client keeps the tunnel up: when the connection drops, it reconnects and binds its names again, waiting
|
|
83
|
+
`--retry-delay` seconds (default 1) and doubling up to `--retry-max` (default 30). Over websocket it also waits for a
|
|
84
|
+
server that isn't reachable yet. A refused login or a rejected name ends the client instead, and `--no-reconnect` makes
|
|
85
|
+
any disconnect end it. `login` saves these flags like the others and `connect` overrides them. Binding a name you
|
|
86
|
+
already hold moves it to the new connection; other identities are refused.
|
|
87
|
+
|
|
88
|
+
The websocket transport goes through an HTTP proxy from `HTTPS_PROXY` (or `HTTP_PROXY`) unless `NO_PROXY` excludes the
|
|
89
|
+
server. The ssh transport follows your SSH config, so a `ProxyJump` or `ProxyCommand` there applies.
|
|
90
|
+
|
|
91
|
+
Plain `ssh` works without the client:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
ssh -T -p 2222 -R '*.foo:80:localhost:3000' tunnel.example.com
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Server
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
uv tool install 'wildburrow[server]'
|
|
101
|
+
wildburrow service install
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`service install` asks for the domain, whether to serve HTTPS and through which DNS provider, and which keys may open
|
|
105
|
+
tunnels, then installs the server as a system service and starts it: systemd on Linux, launchd on macOS. It asks for
|
|
106
|
+
sudo itself. Point `tunnel.example.com` and `*.tunnel.example.com` at the server; DNS wildcards match any depth, so
|
|
107
|
+
deeper names resolve too.
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
wildburrow service status # running or not, and where the logs are
|
|
111
|
+
wildburrow service edit # the same questions, answered with the current settings
|
|
112
|
+
wildburrow service uninstall # stop and remove it, asking whether to keep the certificates
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each of them takes the settings as options instead, and then asks nothing:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
wildburrow service install --domain=tunnel.example.com --acme-email=you@example.com \
|
|
119
|
+
--dns=cloudflare --dns-cloudflare-api-token-file=/root/cloudflare-token
|
|
120
|
+
wildburrow service edit --http-port=8080
|
|
121
|
+
wildburrow service uninstall --yes --purge
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Without a DNS provider the server serves plain HTTP on port 80. With one, it gets wildcard certificates by proving it
|
|
125
|
+
owns the domain through DNS: port 80 redirects bound names to HTTPS, and port 443 terminates TLS, picking the
|
|
126
|
+
certificate by SNI. A request whose `Host` isn't covered by the certificate of its connection gets `421`. Try
|
|
127
|
+
`--acme-directory=https://acme-staging-v02.api.letsencrypt.org/directory` until the first certificate comes through, so
|
|
128
|
+
a mistake doesn't run into Let's Encrypt's rate limits.
|
|
129
|
+
|
|
130
|
+
Unless told otherwise, the service lets in the keys in your `~/.ssh/authorized_keys`, copied when you install or edit.
|
|
131
|
+
Its settings live in `/etc/wildburrow`, readable only by root and the service.
|
|
132
|
+
|
|
133
|
+
On Linux, an install under a home directory, which is where `uv tool install` puts it, runs as its owner with that
|
|
134
|
+
home read-only; one outside any home, like `/opt`, runs as a throwaway user that systemd makes up. Either way it can
|
|
135
|
+
bind ports 80 and 443 and nothing more. On macOS it runs as the install's owner, which macOS lets bind those ports.
|
|
136
|
+
|
|
137
|
+
### Foreground
|
|
138
|
+
|
|
139
|
+
`wildburrow serve` runs the server in the foreground with the same settings, read from options, an `--env-file` of
|
|
140
|
+
`KEY=VALUE` lines, or the environment:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
sudo wildburrow serve --domain=tunnel.example.com
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Docker
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
docker run -d --restart unless-stopped -p 80:80 -p 443:443 -p 2222:2222 -v wildburrow-data:/data \
|
|
150
|
+
-e WILDBURROW_DOMAIN=tunnel.example.com \
|
|
151
|
+
-e WILDBURROW_AUTH_KEY_AUTHORIZED_KEYS="$(cat ~/.ssh/id_ed25519.pub)" \
|
|
152
|
+
-e WILDBURROW_DNS=cloudflare -e WILDBURROW_DNS_CLOUDFLARE_API_TOKEN=... \
|
|
153
|
+
-e WILDBURROW_ACME_EMAIL=you@example.com \
|
|
154
|
+
ghcr.io/isik-kaplan/wildburrow
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The image comes in versions matching the package, for amd64 and arm64, and runs on Docker Desktop for macOS too.
|
|
158
|
+
`examples/docker-compose.traefik.yml` runs it behind Traefik with TLS passthrough and PROXY protocol, for hosts where
|
|
159
|
+
another proxy already owns ports 80 and 443.
|
|
160
|
+
|
|
161
|
+
### Settings
|
|
162
|
+
|
|
163
|
+
Every setting is an option of `serve`, `service install` and `service edit`, and an environment variable for `serve`:
|
|
164
|
+
`--http-port 8080` is `WILDBURROW_HTTP_PORT=8080`, and an option wins over the variable. Adding `-file` to an option,
|
|
165
|
+
or `_FILE` to a variable, reads the value from a file, for secrets and multi-line values:
|
|
166
|
+
`--auth-key-authorized-keys-file=~/keys`. `wildburrow serve --help` lists them all, plugins included. A service has
|
|
167
|
+
no `--data-dir`; its service manager decides where its data goes.
|
|
168
|
+
|
|
169
|
+
| Variable | Default | |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `WILDBURROW_DOMAIN` | required | Base domain tunnels live under |
|
|
172
|
+
| `WILDBURROW_TRANSPORTS` | `ssh` | Transport plugins: `ssh`, `websocket` |
|
|
173
|
+
| `WILDBURROW_AUTH` | `key` | Auth plugins, tried in order: `key`, `password`, `token` |
|
|
174
|
+
| `WILDBURROW_DATA_DIR` | `~/.local/state/wildburrow` | Host key, certificates, ACME account; `/data` in the image |
|
|
175
|
+
| `WILDBURROW_HTTP_HOST` / `_HTTP_PORT` | `0.0.0.0` / `80` | Plain HTTP listener |
|
|
176
|
+
| `WILDBURROW_HTTPS_HOST` / `_HTTPS_PORT` | `0.0.0.0` / `443` | TLS listener, when `WILDBURROW_DNS` is set |
|
|
177
|
+
| `WILDBURROW_HTTP_PROXY_PROTOCOL` / `_HTTPS_PROXY_PROTOCOL` | `false` | Require a PROXY v1/v2 header |
|
|
178
|
+
| `WILDBURROW_PROXY_TRUSTED` | private ranges | CIDRs allowed to send PROXY headers |
|
|
179
|
+
| `WILDBURROW_MAX_DEPTH` | `5` | Maximum labels in a name, `*` included |
|
|
180
|
+
| `WILDBURROW_MAX_HEADER_BYTES` | `65536` | Largest accepted request head |
|
|
181
|
+
| `WILDBURROW_HEADER_TIMEOUT` | `30` | Seconds for the TLS handshake and request head |
|
|
182
|
+
| `WILDBURROW_DNS` | | DNS plugin for DNS-01: `cloudflare` |
|
|
183
|
+
| `WILDBURROW_DNS_MANAGE_RECORDS` | `false` | Create a CNAME for nested wildcards |
|
|
184
|
+
| `WILDBURROW_ACME_DIRECTORY` | Let's Encrypt | ACME directory URL (staging, ZeroSSL, ...) |
|
|
185
|
+
| `WILDBURROW_ACME_EMAIL` | | ACME account contact |
|
|
186
|
+
| `WILDBURROW_ACME_PROPAGATION_SECONDS` | `30` | Wait after publishing a challenge record |
|
|
187
|
+
| `WILDBURROW_RENEW_BEFORE_DAYS` | `30` | Renew certificates in use this close to expiry |
|
|
188
|
+
| `WILDBURROW_SSH_PORT` | `2222` | SSH transport port (`_HOST`, `_LOGIN_TIMEOUT` too) |
|
|
189
|
+
| `WILDBURROW_WEBSOCKET_OPEN_TIMEOUT` | `10` | Seconds a client has to open a visitor stream |
|
|
190
|
+
| `WILDBURROW_AUTH_KEY_AUTHORIZED_KEYS` | `~/.ssh/authorized_keys` | OpenSSH public keys, one per line |
|
|
191
|
+
| `WILDBURROW_AUTH_PASSWORD_HASH` | | argon2 hash from `wildburrow hash-password` |
|
|
192
|
+
| `WILDBURROW_AUTH_PASSWORD_MAX_FAILURES` | `5` | Failed passwords per address before blocking |
|
|
193
|
+
| `WILDBURROW_AUTH_PASSWORD_BLOCK_SECONDS` | `900` | How long failures count against an address |
|
|
194
|
+
| `WILDBURROW_AUTH_TOKEN_HASHES` | | `name:sha256` lines from `wildburrow token NAME` |
|
|
195
|
+
| `WILDBURROW_DNS_CLOUDFLARE_API_TOKEN` | | Token with DNS Edit and Zone Read |
|
|
196
|
+
|
|
197
|
+
## Plugins
|
|
198
|
+
|
|
199
|
+
Transports, auth methods and DNS providers are plugins registered through entry points: `wildburrow.transports`,
|
|
200
|
+
`wildburrow.auth` and `wildburrow.dns`. A plugin class has a `Settings` dataclass, filled from
|
|
201
|
+
`WILDBURROW_<NAME>_*` (transports), `WILDBURROW_AUTH_<NAME>_*` or `WILDBURROW_DNS_<NAME>_*`, and is constructed with
|
|
202
|
+
it. Its fields become `serve` options the same way. A DNS plugin implements `create_txt`, `delete_txt`, `ensure_cname`
|
|
203
|
+
and `aclose`; see `wildburrow/dns/base.py`.
|
|
204
|
+
Install a package that registers an entry point and select it by name.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
uv sync --all-extras
|
|
210
|
+
uv run pytest -n auto # needs Docker for the Pebble ACME tests
|
|
211
|
+
uv run pytest -n auto -m "not acme and not system" # without Docker
|
|
212
|
+
uv run pytest -m system --no-cov tests/system # installs the real service; CI runs it on Linux and macOS
|
|
213
|
+
uv run ruff check . && uv run ruff format --check .
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Coverage stays at 100%, and every mutant has to be killed. The mutation run re-tests only what changed since the last
|
|
217
|
+
run in `mutants/`, both source and tests; CI keeps that directory cached between runs.
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
uv run python -m scripts.mutation run # re-test what changed
|
|
221
|
+
uv run python -m scripts.mutation run --full # re-test everything
|
|
222
|
+
uv run python -m scripts.mutation run --shard commands # one shard: commands, service, client, ...
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
CI runs each shard as a job of its own with its own cache, so a cold run takes minutes and a red shard says where
|
|
226
|
+
to look. There is one pass: each mutant runs against the tests traced to it, the test that killed it last time and
|
|
227
|
+
those that killed its siblings first, and a survivor is a test to write. mutmut leaves decorated functions alone,
|
|
228
|
+
so a decorated function only hands off to a plain one.
|
|
229
|
+
|
|
230
|
+
Every push to `master` runs the tests and the mutation run, then publishes to PyPI and `ghcr.io` when the version in
|
|
231
|
+
`pyproject.toml` isn't on PyPI yet.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# wildburrow
|
|
2
|
+
|
|
3
|
+
Self-hosted HTTPS tunnels on your own domain, with wildcard names at any depth and certificates issued on demand.
|
|
4
|
+
It runs on Linux and macOS.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
wildburrow 3000 myapp # https://myapp.tunnel.example.com
|
|
8
|
+
wildburrow 3000 '*.foo' # https://<anything>.foo.tunnel.example.com
|
|
9
|
+
wildburrow 3000 '*.bar.foo' # https://<anything>.bar.foo.tunnel.example.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
A wildcard covers exactly one label, like a TLS wildcard certificate: `*.foo` serves `api.foo` but not `x.api.foo`.
|
|
13
|
+
An exact name wins over a wildcard. Quote `*` names so your shell doesn't expand them.
|
|
14
|
+
|
|
15
|
+
Requests reach your local app unchanged, so websockets, server-sent events and streaming responses work as they do
|
|
16
|
+
locally. With HTTPS, visitors use `wss://` and your app sees plain `ws://`.
|
|
17
|
+
|
|
18
|
+
When a name needs a certificate that doesn't exist yet, the server gets `*.<parent>` from an ACME CA through a DNS-01
|
|
19
|
+
challenge while your client waits, then confirms the bind. Later binds under the same parent are instant.
|
|
20
|
+
|
|
21
|
+
## Client
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv tool install wildburrow # or: pipx install wildburrow
|
|
25
|
+
wildburrow login # asks for the server and how to connect to it
|
|
26
|
+
wildburrow 3000 '*.foo'
|
|
27
|
+
wildburrow logout # forgets the saved server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`login` keeps its answers in `~/.config/wildburrow/config.toml` until you log in again or out; nothing in it expires,
|
|
31
|
+
and every tunnel logs in afresh. Given options, `wildburrow login tunnel.example.com --transport websocket`, it asks
|
|
32
|
+
nothing and saves what you passed.
|
|
33
|
+
|
|
34
|
+
Two transports are available:
|
|
35
|
+
|
|
36
|
+
| Transport | How it connects | Auth methods |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `ssh` (default) | your `ssh` binary, port 2222, one `-R` per name | `key`, `password` |
|
|
39
|
+
| `websocket` | `wss://tunnel.example.com` on 443 | `key` (signed with `ssh-keygen -Y sign`), `password`, `token` |
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
wildburrow login tunnel.example.com --transport websocket --auth key --identity ~/.ssh/id_ed25519
|
|
43
|
+
wildburrow login tunnel.example.com --transport websocket --auth token --token wb_...
|
|
44
|
+
WILDBURROW_TOKEN=wb_... wildburrow 3000 app # token from the environment
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The client keeps the tunnel up: when the connection drops, it reconnects and binds its names again, waiting
|
|
48
|
+
`--retry-delay` seconds (default 1) and doubling up to `--retry-max` (default 30). Over websocket it also waits for a
|
|
49
|
+
server that isn't reachable yet. A refused login or a rejected name ends the client instead, and `--no-reconnect` makes
|
|
50
|
+
any disconnect end it. `login` saves these flags like the others and `connect` overrides them. Binding a name you
|
|
51
|
+
already hold moves it to the new connection; other identities are refused.
|
|
52
|
+
|
|
53
|
+
The websocket transport goes through an HTTP proxy from `HTTPS_PROXY` (or `HTTP_PROXY`) unless `NO_PROXY` excludes the
|
|
54
|
+
server. The ssh transport follows your SSH config, so a `ProxyJump` or `ProxyCommand` there applies.
|
|
55
|
+
|
|
56
|
+
Plain `ssh` works without the client:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
ssh -T -p 2222 -R '*.foo:80:localhost:3000' tunnel.example.com
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Server
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
uv tool install 'wildburrow[server]'
|
|
66
|
+
wildburrow service install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`service install` asks for the domain, whether to serve HTTPS and through which DNS provider, and which keys may open
|
|
70
|
+
tunnels, then installs the server as a system service and starts it: systemd on Linux, launchd on macOS. It asks for
|
|
71
|
+
sudo itself. Point `tunnel.example.com` and `*.tunnel.example.com` at the server; DNS wildcards match any depth, so
|
|
72
|
+
deeper names resolve too.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
wildburrow service status # running or not, and where the logs are
|
|
76
|
+
wildburrow service edit # the same questions, answered with the current settings
|
|
77
|
+
wildburrow service uninstall # stop and remove it, asking whether to keep the certificates
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each of them takes the settings as options instead, and then asks nothing:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
wildburrow service install --domain=tunnel.example.com --acme-email=you@example.com \
|
|
84
|
+
--dns=cloudflare --dns-cloudflare-api-token-file=/root/cloudflare-token
|
|
85
|
+
wildburrow service edit --http-port=8080
|
|
86
|
+
wildburrow service uninstall --yes --purge
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Without a DNS provider the server serves plain HTTP on port 80. With one, it gets wildcard certificates by proving it
|
|
90
|
+
owns the domain through DNS: port 80 redirects bound names to HTTPS, and port 443 terminates TLS, picking the
|
|
91
|
+
certificate by SNI. A request whose `Host` isn't covered by the certificate of its connection gets `421`. Try
|
|
92
|
+
`--acme-directory=https://acme-staging-v02.api.letsencrypt.org/directory` until the first certificate comes through, so
|
|
93
|
+
a mistake doesn't run into Let's Encrypt's rate limits.
|
|
94
|
+
|
|
95
|
+
Unless told otherwise, the service lets in the keys in your `~/.ssh/authorized_keys`, copied when you install or edit.
|
|
96
|
+
Its settings live in `/etc/wildburrow`, readable only by root and the service.
|
|
97
|
+
|
|
98
|
+
On Linux, an install under a home directory, which is where `uv tool install` puts it, runs as its owner with that
|
|
99
|
+
home read-only; one outside any home, like `/opt`, runs as a throwaway user that systemd makes up. Either way it can
|
|
100
|
+
bind ports 80 and 443 and nothing more. On macOS it runs as the install's owner, which macOS lets bind those ports.
|
|
101
|
+
|
|
102
|
+
### Foreground
|
|
103
|
+
|
|
104
|
+
`wildburrow serve` runs the server in the foreground with the same settings, read from options, an `--env-file` of
|
|
105
|
+
`KEY=VALUE` lines, or the environment:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
sudo wildburrow serve --domain=tunnel.example.com
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Docker
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
docker run -d --restart unless-stopped -p 80:80 -p 443:443 -p 2222:2222 -v wildburrow-data:/data \
|
|
115
|
+
-e WILDBURROW_DOMAIN=tunnel.example.com \
|
|
116
|
+
-e WILDBURROW_AUTH_KEY_AUTHORIZED_KEYS="$(cat ~/.ssh/id_ed25519.pub)" \
|
|
117
|
+
-e WILDBURROW_DNS=cloudflare -e WILDBURROW_DNS_CLOUDFLARE_API_TOKEN=... \
|
|
118
|
+
-e WILDBURROW_ACME_EMAIL=you@example.com \
|
|
119
|
+
ghcr.io/isik-kaplan/wildburrow
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The image comes in versions matching the package, for amd64 and arm64, and runs on Docker Desktop for macOS too.
|
|
123
|
+
`examples/docker-compose.traefik.yml` runs it behind Traefik with TLS passthrough and PROXY protocol, for hosts where
|
|
124
|
+
another proxy already owns ports 80 and 443.
|
|
125
|
+
|
|
126
|
+
### Settings
|
|
127
|
+
|
|
128
|
+
Every setting is an option of `serve`, `service install` and `service edit`, and an environment variable for `serve`:
|
|
129
|
+
`--http-port 8080` is `WILDBURROW_HTTP_PORT=8080`, and an option wins over the variable. Adding `-file` to an option,
|
|
130
|
+
or `_FILE` to a variable, reads the value from a file, for secrets and multi-line values:
|
|
131
|
+
`--auth-key-authorized-keys-file=~/keys`. `wildburrow serve --help` lists them all, plugins included. A service has
|
|
132
|
+
no `--data-dir`; its service manager decides where its data goes.
|
|
133
|
+
|
|
134
|
+
| Variable | Default | |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `WILDBURROW_DOMAIN` | required | Base domain tunnels live under |
|
|
137
|
+
| `WILDBURROW_TRANSPORTS` | `ssh` | Transport plugins: `ssh`, `websocket` |
|
|
138
|
+
| `WILDBURROW_AUTH` | `key` | Auth plugins, tried in order: `key`, `password`, `token` |
|
|
139
|
+
| `WILDBURROW_DATA_DIR` | `~/.local/state/wildburrow` | Host key, certificates, ACME account; `/data` in the image |
|
|
140
|
+
| `WILDBURROW_HTTP_HOST` / `_HTTP_PORT` | `0.0.0.0` / `80` | Plain HTTP listener |
|
|
141
|
+
| `WILDBURROW_HTTPS_HOST` / `_HTTPS_PORT` | `0.0.0.0` / `443` | TLS listener, when `WILDBURROW_DNS` is set |
|
|
142
|
+
| `WILDBURROW_HTTP_PROXY_PROTOCOL` / `_HTTPS_PROXY_PROTOCOL` | `false` | Require a PROXY v1/v2 header |
|
|
143
|
+
| `WILDBURROW_PROXY_TRUSTED` | private ranges | CIDRs allowed to send PROXY headers |
|
|
144
|
+
| `WILDBURROW_MAX_DEPTH` | `5` | Maximum labels in a name, `*` included |
|
|
145
|
+
| `WILDBURROW_MAX_HEADER_BYTES` | `65536` | Largest accepted request head |
|
|
146
|
+
| `WILDBURROW_HEADER_TIMEOUT` | `30` | Seconds for the TLS handshake and request head |
|
|
147
|
+
| `WILDBURROW_DNS` | | DNS plugin for DNS-01: `cloudflare` |
|
|
148
|
+
| `WILDBURROW_DNS_MANAGE_RECORDS` | `false` | Create a CNAME for nested wildcards |
|
|
149
|
+
| `WILDBURROW_ACME_DIRECTORY` | Let's Encrypt | ACME directory URL (staging, ZeroSSL, ...) |
|
|
150
|
+
| `WILDBURROW_ACME_EMAIL` | | ACME account contact |
|
|
151
|
+
| `WILDBURROW_ACME_PROPAGATION_SECONDS` | `30` | Wait after publishing a challenge record |
|
|
152
|
+
| `WILDBURROW_RENEW_BEFORE_DAYS` | `30` | Renew certificates in use this close to expiry |
|
|
153
|
+
| `WILDBURROW_SSH_PORT` | `2222` | SSH transport port (`_HOST`, `_LOGIN_TIMEOUT` too) |
|
|
154
|
+
| `WILDBURROW_WEBSOCKET_OPEN_TIMEOUT` | `10` | Seconds a client has to open a visitor stream |
|
|
155
|
+
| `WILDBURROW_AUTH_KEY_AUTHORIZED_KEYS` | `~/.ssh/authorized_keys` | OpenSSH public keys, one per line |
|
|
156
|
+
| `WILDBURROW_AUTH_PASSWORD_HASH` | | argon2 hash from `wildburrow hash-password` |
|
|
157
|
+
| `WILDBURROW_AUTH_PASSWORD_MAX_FAILURES` | `5` | Failed passwords per address before blocking |
|
|
158
|
+
| `WILDBURROW_AUTH_PASSWORD_BLOCK_SECONDS` | `900` | How long failures count against an address |
|
|
159
|
+
| `WILDBURROW_AUTH_TOKEN_HASHES` | | `name:sha256` lines from `wildburrow token NAME` |
|
|
160
|
+
| `WILDBURROW_DNS_CLOUDFLARE_API_TOKEN` | | Token with DNS Edit and Zone Read |
|
|
161
|
+
|
|
162
|
+
## Plugins
|
|
163
|
+
|
|
164
|
+
Transports, auth methods and DNS providers are plugins registered through entry points: `wildburrow.transports`,
|
|
165
|
+
`wildburrow.auth` and `wildburrow.dns`. A plugin class has a `Settings` dataclass, filled from
|
|
166
|
+
`WILDBURROW_<NAME>_*` (transports), `WILDBURROW_AUTH_<NAME>_*` or `WILDBURROW_DNS_<NAME>_*`, and is constructed with
|
|
167
|
+
it. Its fields become `serve` options the same way. A DNS plugin implements `create_txt`, `delete_txt`, `ensure_cname`
|
|
168
|
+
and `aclose`; see `wildburrow/dns/base.py`.
|
|
169
|
+
Install a package that registers an entry point and select it by name.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
uv sync --all-extras
|
|
175
|
+
uv run pytest -n auto # needs Docker for the Pebble ACME tests
|
|
176
|
+
uv run pytest -n auto -m "not acme and not system" # without Docker
|
|
177
|
+
uv run pytest -m system --no-cov tests/system # installs the real service; CI runs it on Linux and macOS
|
|
178
|
+
uv run ruff check . && uv run ruff format --check .
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Coverage stays at 100%, and every mutant has to be killed. The mutation run re-tests only what changed since the last
|
|
182
|
+
run in `mutants/`, both source and tests; CI keeps that directory cached between runs.
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
uv run python -m scripts.mutation run # re-test what changed
|
|
186
|
+
uv run python -m scripts.mutation run --full # re-test everything
|
|
187
|
+
uv run python -m scripts.mutation run --shard commands # one shard: commands, service, client, ...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
CI runs each shard as a job of its own with its own cache, so a cold run takes minutes and a red shard says where
|
|
191
|
+
to look. There is one pass: each mutant runs against the tests traced to it, the test that killed it last time and
|
|
192
|
+
those that killed its siblings first, and a survivor is a test to write. mutmut leaves decorated functions alone,
|
|
193
|
+
so a decorated function only hands off to a plain one.
|
|
194
|
+
|
|
195
|
+
Every push to `master` runs the tests and the mutation run, then publishes to PyPI and `ghcr.io` when the version in
|
|
196
|
+
`pyproject.toml` isn't on PyPI yet.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.32.4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wildburrow"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Self-hosted HTTP tunnels with on-demand wildcard certificates at any depth"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Işık Kaplan" }]
|
|
13
|
+
keywords = ["tunnel", "ngrok", "reverse-proxy", "ssh", "websocket", "acme", "letsencrypt", "wildcard"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Programming Language :: Python :: 3.15",
|
|
25
|
+
"Topic :: Internet :: Proxy Servers",
|
|
26
|
+
"Topic :: System :: Networking",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.14.4"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"click==8.5.0",
|
|
31
|
+
"questionary==2.1.1",
|
|
32
|
+
"websockets==17.2",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Repository = "https://github.com/isik-kaplan/wildburrow"
|
|
37
|
+
Issues = "https://github.com/isik-kaplan/wildburrow/issues"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
server = [
|
|
41
|
+
"acme==5.8.0",
|
|
42
|
+
"argon2-cffi==25.1.0",
|
|
43
|
+
"asyncssh==2.24.1",
|
|
44
|
+
"cryptography==50.0.2",
|
|
45
|
+
"httpx==0.28.1",
|
|
46
|
+
"josepy==2.2.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
wildburrow = "wildburrow.cli:main"
|
|
51
|
+
|
|
52
|
+
[project.entry-points."wildburrow.transports"]
|
|
53
|
+
ssh = "wildburrow.transports.ssh:SSHTransport"
|
|
54
|
+
websocket = "wildburrow.transports.websocket:WebSocketTransport"
|
|
55
|
+
|
|
56
|
+
[project.entry-points."wildburrow.auth"]
|
|
57
|
+
key = "wildburrow.auth.keys:KeyAuth"
|
|
58
|
+
password = "wildburrow.auth.password:PasswordAuth"
|
|
59
|
+
token = "wildburrow.auth.token:TokenAuth"
|
|
60
|
+
|
|
61
|
+
[project.entry-points."wildburrow.dns"]
|
|
62
|
+
cloudflare = "wildburrow.dns.cloudflare:CloudflareDNS"
|
|
63
|
+
|
|
64
|
+
[dependency-groups]
|
|
65
|
+
dev = [
|
|
66
|
+
"hypothesis==6.169.0",
|
|
67
|
+
"mutmut==3.8.0",
|
|
68
|
+
"pytest==9.1.1",
|
|
69
|
+
"pytest-asyncio==1.4.0",
|
|
70
|
+
"pytest-cov==7.1.0",
|
|
71
|
+
"pytest-timeout==2.4.0",
|
|
72
|
+
"pytest-xdist==3.8.0",
|
|
73
|
+
"ruff==0.17.0",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.hatch.build.targets.sdist]
|
|
77
|
+
include = ["src", "tests", "README.md", "LICENSE"]
|
|
78
|
+
|
|
79
|
+
[tool.hatch.build.targets.wheel]
|
|
80
|
+
packages = ["src/wildburrow"]
|
|
81
|
+
|
|
82
|
+
[tool.uv]
|
|
83
|
+
default-groups = ["dev"]
|
|
84
|
+
|
|
85
|
+
[tool.ruff]
|
|
86
|
+
line-length = 120
|
|
87
|
+
target-version = "py314"
|
|
88
|
+
|
|
89
|
+
[tool.ruff.lint]
|
|
90
|
+
select = ["E", "F", "W", "I", "B", "UP", "S", "ASYNC", "RUF", "SIM", "PT"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint.per-file-ignores]
|
|
93
|
+
"tests/**" = ["S101", "S105", "S106", "S603", "S607", "ASYNC110"]
|
|
94
|
+
|
|
95
|
+
[tool.pytest.ini_options]
|
|
96
|
+
testpaths = ["tests"]
|
|
97
|
+
asyncio_mode = "auto"
|
|
98
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
99
|
+
addopts = "--strict-markers --cov --cov-branch --cov-report=term-missing -m 'not system'"
|
|
100
|
+
markers = [
|
|
101
|
+
"e2e: drives real clients against a running server",
|
|
102
|
+
"acme: needs Docker for a local Pebble ACME server",
|
|
103
|
+
"system: installs the real service, so it needs root through sudo without a password",
|
|
104
|
+
]
|
|
105
|
+
filterwarnings = ["error"]
|
|
106
|
+
timeout = 120
|
|
107
|
+
# Ends the run outright: raised inside the code instead, a timeout can be caught and the hang carries on.
|
|
108
|
+
timeout_method = "thread"
|
|
109
|
+
|
|
110
|
+
[tool.coverage.run]
|
|
111
|
+
source = ["wildburrow", "scripts"]
|
|
112
|
+
branch = true
|
|
113
|
+
|
|
114
|
+
[tool.coverage.report]
|
|
115
|
+
fail_under = 100
|
|
116
|
+
show_missing = true
|
|
117
|
+
exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError", "\\.\\.\\.$"]
|
|
118
|
+
|
|
119
|
+
[tool.mutmut]
|
|
120
|
+
source_paths = ["src/wildburrow"]
|
|
121
|
+
also_copy = ["scripts"]
|
|
122
|
+
# The trampolines add statements no coverage gate can account for, and a fixed seed keeps a verdict repeatable.
|
|
123
|
+
# The slowest test takes seconds, so a test still going at 30 is a mutant hanging it, not a slow test.
|
|
124
|
+
pytest_add_cli_args = ["--no-cov", "--hypothesis-seed=0", "--timeout=30", "-p", "scripts.mutation_killers"]
|
|
125
|
+
# mutmut stops a run at (estimated + constant) * 15 seconds; this keeps that past the 30 a hung test gets, so the test
|
|
126
|
+
# timeout fails it the way it would fail a run without mutmut.
|
|
127
|
+
timeout_constant = 3
|
|
128
|
+
# Starting a flag at False or an optional at None leaves one mutant, the other falsy value, which nothing can tell apart.
|
|
129
|
+
do_not_mutate_patterns = ['^\s*[\w.]+(: [^=]+)? = (False|None)$']
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import ClassVar, Protocol
|
|
3
|
+
|
|
4
|
+
from wildburrow.core.types import Credential, Identity, Peer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthProvider(Protocol):
|
|
8
|
+
handles: ClassVar[tuple[type, ...]]
|
|
9
|
+
|
|
10
|
+
async def authenticate(self, credential: Credential, peer: Peer) -> Identity | None: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Authenticator:
|
|
14
|
+
def __init__(self, providers: Sequence[AuthProvider]) -> None:
|
|
15
|
+
self._providers = tuple(providers)
|
|
16
|
+
|
|
17
|
+
def accepts(self, kind: type) -> bool:
|
|
18
|
+
return any(kind in provider.handles for provider in self._providers)
|
|
19
|
+
|
|
20
|
+
async def authenticate(self, credential: Credential, peer: Peer) -> Identity | None:
|
|
21
|
+
for provider in self._providers:
|
|
22
|
+
if type(credential) in provider.handles and (identity := await provider.authenticate(credential, peer)):
|
|
23
|
+
return identity
|
|
24
|
+
return None
|