This commit is contained in:
+2
-1
@@ -12,12 +12,13 @@ Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorl
|
|||||||
- De bronnenpagina toont 21 actieve bronnen, 12 waaklijstwerkgevers en de actieve GeoNames-afstandsstatus; waaklijstbronnen vallen buiten bulkacties en geplande scans.
|
- De bronnenpagina toont 21 actieve bronnen, 12 waaklijstwerkgevers en de actieve GeoNames-afstandsstatus; waaklijstbronnen vallen buiten bulkacties en geplande scans.
|
||||||
- HTTP-, robots- en IMAP-verbindingen gebruiken expliciet de gepinde `certifi`-CA-bundel met verplichte hostnaam- en certificaatcontrole. Gemeente Mol en Ziekenhuis Geel werken daarmee zonder TLS-bypass.
|
- HTTP-, robots- en IMAP-verbindingen gebruiken expliciet de gepinde `certifi`-CA-bundel met verplichte hostnaam- en certificaatcontrole. Gemeente Mol en Ziekenhuis Geel werken daarmee zonder TLS-bypass.
|
||||||
- Celery gebruikt expliciet queue `default`, zodat bronruns en andere standaardtaken daadwerkelijk door de productie-worker worden geconsumeerd.
|
- Celery gebruikt expliciet queue `default`, zodat bronruns en andere standaardtaken daadwerkelijk door de productie-worker worden geconsumeerd.
|
||||||
|
- De fetcher controleert het werkelijk verbonden peer-IP en accepteert daardoor veilige publieke CDN-rotatie zonder de SSRF-blokkade voor private adressen te versoepelen.
|
||||||
|
|
||||||
### Verificatie
|
### Verificatie
|
||||||
|
|
||||||
- De 8 nieuwe actieve bronnen leverden samen 53 kaarten per cyclus en voltooiden elk twee livecycli zonder parserwarnings; de tweede cycli maakten geen nieuwe dubbelen aan.
|
- De 8 nieuwe actieve bronnen leverden samen 53 kaarten per cyclus en voltooiden elk twee livecycli zonder parserwarnings; de tweede cycli maakten geen nieuwe dubbelen aan.
|
||||||
- GeoNames valideerde 2.781 unieke Belgische postcode-/gemeenterijen; bron- en afgeleide SHA-256-checksums zijn vastgelegd en het actieve profiel is opnieuw gescoord.
|
- GeoNames valideerde 2.781 unieke Belgische postcode-/gemeenterijen; bron- en afgeleide SHA-256-checksums zijn vastgelegd en het actieve profiel is opnieuw gescoord.
|
||||||
- 216 tests geslaagd, 2 optionele Playwrightvarianten overgeslagen en 84,42% branch-aware codedekking; Ruff, Django, migraties, taakledger en repositoryvalidatie zijn groen.
|
- 218 tests geslaagd, 2 optionele Playwrightvarianten overgeslagen en 84,46% branch-aware codedekking; Ruff, Django, migraties, taakledger en repositoryvalidatie zijn groen.
|
||||||
- De Codex-browsercontrole bevestigde 21 actieve bronnen, 12 waaklijstkaarten, 8 nieuwe actieve werkgevers, één `h1`, een lege console en geen horizontale overflow op desktop of 390 px.
|
- De Codex-browsercontrole bevestigde 21 actieve bronnen, 12 waaklijstkaarten, 8 nieuwe actieve werkgevers, één `h1`, een lege console en geen horizontale overflow op desktop of 390 px.
|
||||||
|
|
||||||
## 0.2.9 — 2026-07-22
|
## 0.2.9 — 2026-07-22
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import ipaddress
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -53,6 +54,38 @@ class FetchTimeoutError(FetchError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _connected_peer_ip(
|
||||||
|
response: httpx.Response,
|
||||||
|
) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
||||||
|
"""Return the actual remote peer when httpcore exposes its network stream."""
|
||||||
|
stream = response.extensions.get("network_stream")
|
||||||
|
if stream is None or not hasattr(stream, "get_extra_info"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
server_address = stream.get_extra_info("server_addr")
|
||||||
|
raw_address = server_address[0] if server_address else None
|
||||||
|
return ipaddress.ip_address(raw_address) if raw_address else None
|
||||||
|
except (IndexError, TypeError, ValueError, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_connected_peer(
|
||||||
|
response: httpx.Response,
|
||||||
|
*,
|
||||||
|
before: ValidatedUrl,
|
||||||
|
after: ValidatedUrl,
|
||||||
|
) -> None:
|
||||||
|
peer_ip = _connected_peer_ip(response)
|
||||||
|
if peer_ip is not None:
|
||||||
|
if not peer_ip.is_global:
|
||||||
|
raise FetchError(f"Niet-publiek verbonden IP-adres geblokkeerd: {peer_ip}")
|
||||||
|
return
|
||||||
|
# Mock/custom transports do not always expose the peer socket. Keep the conservative
|
||||||
|
# DNS-overlap fallback for those transports.
|
||||||
|
if not set(before.addresses).intersection(after.addresses):
|
||||||
|
raise FetchError("DNS-rebindcontrole faalde bij het benaderen van bron.")
|
||||||
|
|
||||||
|
|
||||||
def parse_retry_after(value: str | None) -> int | None:
|
def parse_retry_after(value: str | None) -> int | None:
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
@@ -127,26 +160,22 @@ def fetch_url(
|
|||||||
verify=trusted_tls_context(),
|
verify=trusted_tls_context(),
|
||||||
)
|
)
|
||||||
current_url = url
|
current_url = url
|
||||||
previous_validation: ValidatedUrl | None = None
|
|
||||||
try:
|
try:
|
||||||
for _ in range(settings.FETCHER_MAX_REDIRECTS + 1):
|
for _ in range(settings.FETCHER_MAX_REDIRECTS + 1):
|
||||||
validation = validate_public_url(
|
validation = validate_public_url(
|
||||||
current_url,
|
current_url,
|
||||||
allow_nonstandard_ports=settings.FETCHER_ALLOW_NONSTANDARD_PORTS,
|
allow_nonstandard_ports=settings.FETCHER_ALLOW_NONSTANDARD_PORTS,
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
previous_validation is not None
|
|
||||||
and validation.hostname == previous_validation.hostname
|
|
||||||
and not set(validation.addresses).intersection(previous_validation.addresses)
|
|
||||||
):
|
|
||||||
raise FetchError("DNS-rebindcontrole faalde bij redirect naar dezelfde host.")
|
|
||||||
response = http_client.get(current_url, headers=headers)
|
response = http_client.get(current_url, headers=headers)
|
||||||
post_validation = validate_public_url(
|
post_validation = validate_public_url(
|
||||||
str(response.url) if response.url else current_url,
|
str(response.url) if response.url else current_url,
|
||||||
allow_nonstandard_ports=settings.FETCHER_ALLOW_NONSTANDARD_PORTS,
|
allow_nonstandard_ports=settings.FETCHER_ALLOW_NONSTANDARD_PORTS,
|
||||||
)
|
)
|
||||||
if not set(validation.addresses).intersection(set(post_validation.addresses)):
|
_validate_connected_peer(
|
||||||
raise FetchError("DNS-rebindcontrole faalde bij het benaderen van bron.")
|
response,
|
||||||
|
before=validation,
|
||||||
|
after=post_validation,
|
||||||
|
)
|
||||||
if response.status_code in {301, 302, 303, 307, 308}:
|
if response.status_code in {301, 302, 303, 307, 308}:
|
||||||
location = response.headers.get("location")
|
location = response.headers.get("location")
|
||||||
if not location:
|
if not location:
|
||||||
@@ -155,7 +184,6 @@ def fetch_url(
|
|||||||
next_decision = assess_url(current_url, source=source)
|
next_decision = assess_url(current_url, source=source)
|
||||||
if not next_decision.allowed:
|
if not next_decision.allowed:
|
||||||
raise PolicyBlockedError(next_decision.reason)
|
raise PolicyBlockedError(next_decision.reason)
|
||||||
previous_validation = validation
|
|
||||||
continue
|
continue
|
||||||
if response.status_code == 304:
|
if response.status_code == 304:
|
||||||
return FetchedDocument(url, current_url, 304, dict(response.headers), b"")
|
return FetchedDocument(url, current_url, 304, dict(response.headers), b"")
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Release-audit op 2026-07-21:
|
|||||||
- backlog- en repositoryvalidatie geslaagd;
|
- backlog- en repositoryvalidatie geslaagd;
|
||||||
- de lockfile gebruikt publieke PyPI-bronnen in plaats van een niet-overdraagbare interne registry;
|
- de lockfile gebruikt publieke PyPI-bronnen in plaats van een niet-overdraagbare interne registry;
|
||||||
- releaseblokkers hersteld in template-rendering, JSON/BOM-verwerking, leases, feedback, dossierautorisatie, ATS-provenance, employer-resolutie en DNS-rebindcontrole;
|
- releaseblokkers hersteld in template-rendering, JSON/BOM-verwerking, leases, feedback, dossierautorisatie, ATS-provenance, employer-resolutie en DNS-rebindcontrole;
|
||||||
|
- de productie-fetcher valideert het verbonden peer-IP, zodat legitieme CDN-DNS-rotatie bij DAF en Xplore Group niet als rebind wordt afgewezen terwijl private peers geblokkeerd blijven;
|
||||||
- een echte Dockerimagebuild op de Unraid-server, migratiecontrole en live LAN-smoke voor liveness, readiness, login en static assets zijn geslaagd;
|
- een echte Dockerimagebuild op de Unraid-server, migratiecontrole en live LAN-smoke voor liveness, readiness, login en static assets zijn geslaagd;
|
||||||
- deploymenthardening voorkomt gelijktijdige migraties door worker/scheduler, schakelt de Gunicorn-controlsocket uit voor de read-only container en houdt de lokale healthcheckhost toegestaan.
|
- deploymenthardening voorkomt gelijktijdige migraties door worker/scheduler, schakelt de Gunicorn-controlsocket uit voor de read-only container en houdt de lokale healthcheckhost toegestaan.
|
||||||
|
|
||||||
@@ -119,14 +120,15 @@ VR-126-herverificatie op 2026-07-22:
|
|||||||
|
|
||||||
VR-201/VR-205-herverificatie op 2026-07-22:
|
VR-201/VR-205-herverificatie op 2026-07-22:
|
||||||
|
|
||||||
- 216 geslaagde tests en 2 optioneel overgeslagen pytest-Playwrightvarianten; HTML/a11y-fallback geslaagd;
|
- 218 geslaagde tests en 2 optioneel overgeslagen pytest-Playwrightvarianten; HTML/a11y-fallback geslaagd;
|
||||||
- 84,42% branch-aware codedekking;
|
- 84,46% branch-aware codedekking;
|
||||||
- Ruff, Django system check, migratiecontrole, taakledger en repositoryvalidatie geslaagd via `scripts/codex_verify.sh`;
|
- Ruff, Django system check, migratiecontrole, taakledger en repositoryvalidatie geslaagd via `scripts/codex_verify.sh`;
|
||||||
- GeoNames België 2026-04-03 is onder CC BY 4.0 met checksums gedocumenteerd, 2.781 unieke invoerrijen zijn gevalideerd en 2400 Mol is opgelost naar het benaderende postcodecentrum 51.1919, 5.1166;
|
- GeoNames België 2026-04-03 is onder CC BY 4.0 met checksums gedocumenteerd, 2.781 unieke invoerrijen zijn gevalideerd en 2400 Mol is opgelost naar het benaderende postcodecentrum 51.1919, 5.1166;
|
||||||
- Gemeente Mol en zeven bijkomende Kempen-werkgevers passeerden elk twee livecycli zonder parserwarnings; samen leverden zij 53 kaarten per cyclus;
|
- Gemeente Mol en zeven bijkomende Kempen-werkgevers passeerden elk twee livecycli zonder parserwarnings; samen leverden zij 53 kaarten per cyclus;
|
||||||
- twaalf regionale werkgevers staan afzonderlijk als niet-scanbare waaklijst in de UI, ook wanneer zij momenteel geen vacature hebben;
|
- twaalf regionale werkgevers staan afzonderlijk als niet-scanbare waaklijst in de UI, ook wanneer zij momenteel geen vacature hebben;
|
||||||
- Codex-browsercontrole bevestigde 21 actieve bronnen, 12 waaklijstkaarten, de GeoNames-status, één `h1`, geen horizontale overflow op desktop of 390 px en geen consolefouten;
|
- Codex-browsercontrole bevestigde 21 actieve bronnen, 12 waaklijstkaarten, de GeoNames-status, één `h1`, geen horizontale overflow op desktop of 390 px en geen consolefouten;
|
||||||
- de Unraid-deploy gebruikt expliciet Celery-queue `default`, overeenkomstig de queues die de productie-worker consumeert;
|
- de Unraid-deploy gebruikt expliciet Celery-queue `default`, overeenkomstig de queues die de productie-worker consumeert;
|
||||||
|
- de fetcher valideert het werkelijk verbonden publieke peer-IP, zodat legitieme CDN-rotatie van DAF en Xplore Group veilig wordt ondersteund;
|
||||||
- taakledger bevat 44 taken: 38 `done`, 3 `blocked-external`, 3 `deferred` en 0 `ready`.
|
- taakledger bevat 44 taken: 38 `done`, 3 `blocked-external`, 3 `deferred` en 0 `ready`.
|
||||||
|
|
||||||
## Deploymentstatus
|
## Deploymentstatus
|
||||||
@@ -320,7 +322,6 @@ Deze blokkeren onafhankelijke code niet:
|
|||||||
- Unraid-domein, DNS, TLS en reverse proxy;
|
- Unraid-domein, DNS, TLS en reverse proxy;
|
||||||
- containerregistry en credentials;
|
- containerregistry en credentials;
|
||||||
- actieve/voor deze repository geregistreerde Gitea Actions-runner;
|
- actieve/voor deze repository geregistreerde Gitea Actions-runner;
|
||||||
- keuze/verkrijging van een gelicenseerde actuele Belgische postcode-/gemeentedataset.
|
|
||||||
|
|
||||||
## Bekende verificatiebeperking van de aangeleverde basis
|
## Bekende verificatiebeperking van de aangeleverde basis
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Django/Celery
|
|||||||
| ID | Dreiging | Impact | Mitigaties | Verificatie |
|
| ID | Dreiging | Impact | Mitigaties | Verificatie |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| T-01 | SSRF via bron- of vacature-URL | toegang tot LAN, metadata of beheerinterfaces | schema/host/IP/poortcontrole, DNS-resolutie, redirecthercontrole, denylist, geen credentials in URL | `tests/security/test_url_security.py`, `test_policy.py`, fetchertests |
|
| T-01 | SSRF via bron- of vacature-URL | toegang tot LAN, metadata of beheerinterfaces | schema/host/IP/poortcontrole, DNS-resolutie, redirecthercontrole, denylist, geen credentials in URL | `tests/security/test_url_security.py`, `test_policy.py`, fetchertests |
|
||||||
| T-02 | DNS rebinding/tijd-van-controle | request bereikt ander IP dan beoordeeld | iedere fetch en redirect valideert opnieuw DNS-resolutie en vereist consistente IP-set-overlap | `VR-115`, `tests/unit/test_fetcher.py`, aanvullende fetcher negative tests |
|
| T-02 | DNS rebinding/tijd-van-controle | request bereikt ander IP dan beoordeeld | iedere fetch en redirect valideert DNS opnieuw; het werkelijk verbonden peer-IP wordt als publiek gecontroleerd wanneer de transportlaag dit exposeert, anders geldt conservatieve DNS-set-overlap | `VR-115`, `tests/unit/test_fetcher.py`, aanvullende fetcher negative tests |
|
||||||
| T-03 | XSS/HTML-injectie | sessiediefstal of ongewenste navigatie | Bleach allowlist, platte tekst, template autoescaping, CSP/securityheaders | `tests/security/test_sanitize.py`, viewtests |
|
| T-03 | XSS/HTML-injectie | sessiediefstal of ongewenste navigatie | Bleach allowlist, platte tekst, template autoescaping, CSP/securityheaders | `tests/security/test_sanitize.py`, viewtests |
|
||||||
| T-04 | Prompt injection in vacature | AI verandert beleid of voert tools uit | vacaturetekst in data-envelope, schema-output, evidence-validatie, cachefouten met foutcategorie, geen toolrechten, deterministische harde regels, AI optioneel | AI-tests, ADR-0003, `VR-109` evaluaties |
|
| T-04 | Prompt injection in vacature | AI verandert beleid of voert tools uit | vacaturetekst in data-envelope, schema-output, evidence-validatie, cachefouten met foutcategorie, geen toolrechten, deterministische harde regels, AI optioneel | AI-tests, ADR-0003, `VR-109` evaluaties |
|
||||||
| T-05 | Malafide/oversized document | geheugen/CPU/diskuitputting | contenttype-, byte-, timeout- en redirectlimieten; korte retentie | fetchertests, source-runmetrics |
|
| T-05 | Malafide/oversized document | geheugen/CPU/diskuitputting | contenttype-, byte-, timeout- en redirectlimieten; korte retentie | fetchertests, source-runmetrics |
|
||||||
@@ -112,7 +112,7 @@ Een wijziging aan netwerk, mail, AI, rendering, auth, exports of uploads vereist
|
|||||||
|
|
||||||
## Resterende risico's
|
## Resterende risico's
|
||||||
|
|
||||||
- Een applicatielaag kan DNS rebinding niet volledig uitsluiten zonder transportniveau-IP-pinning; dit staat in `VR-115`.
|
- De fetcher controleert het werkelijk verbonden peer-IP wanneer httpcore dat exposeert. Een custom transport dat geen peerinformatie aanbiedt valt terug op DNS-set-overlap; volledige IP-pinning blijft een aanvullend egress-hardeningniveau.
|
||||||
- Brute-force of herhaalde foutieve mutatiepogingen blijven mogelijk, maar zijn beperkt door in-memory rate limiting en blokkades op login/manual-import.
|
- Brute-force of herhaalde foutieve mutatiepogingen blijven mogelijk, maar zijn beperkt door in-memory rate limiting en blokkades op login/manual-import.
|
||||||
- Bronvoorwaarden vereisen menselijke/externe review per domein; automatisering kan dat niet juridisch beslissen.
|
- Bronvoorwaarden vereisen menselijke/externe review per domein; automatisering kan dat niet juridisch beslissen.
|
||||||
- Fuzzy dedupe en featureheuristieken kunnen inhoudelijk verkeerd zijn; provenance, feedback en benchmarkevaluaties beperken maar elimineren dit niet.
|
- Fuzzy dedupe en featureheuristieken kunnen inhoudelijk verkeerd zijn; provenance, feedback en benchmarkevaluaties beperken maar elimineren dit niet.
|
||||||
|
|||||||
+66
-11
@@ -76,28 +76,83 @@ def test_fetcher_respects_rate_limit_and_retry_after(monkeypatch):
|
|||||||
|
|
||||||
def test_fetcher_rejects_dns_rebinding(monkeypatch):
|
def test_fetcher_rejects_dns_rebinding(monkeypatch):
|
||||||
_patch_dns(monkeypatch)
|
_patch_dns(monkeypatch)
|
||||||
|
calls = 0
|
||||||
|
|
||||||
def validate(url: str, **kwargs):
|
def validate(url: str, **kwargs):
|
||||||
if "/final" in url:
|
nonlocal calls
|
||||||
return ValidatedUrl(
|
calls += 1
|
||||||
url=url, hostname="example.org", port=443, addresses=("198.51.100.12",)
|
address = "93.184.216.34" if calls == 1 else "198.51.100.12"
|
||||||
|
return ValidatedUrl(url=url, hostname="example.org", port=443, addresses=(address,))
|
||||||
|
|
||||||
|
monkeypatch.setattr("apps.sources.services.fetcher.validate_public_url", validate)
|
||||||
|
|
||||||
|
client = httpx.Client(
|
||||||
|
transport=httpx.MockTransport(
|
||||||
|
lambda request: httpx.Response(
|
||||||
|
200,
|
||||||
|
headers={"content-type": "text/html; charset=utf-8"},
|
||||||
|
content=b"Vacature",
|
||||||
)
|
)
|
||||||
return ValidatedUrl(url=url, hostname="example.org", port=443, addresses=("93.184.216.34",))
|
)
|
||||||
|
)
|
||||||
|
with pytest.raises(FetchError, match="DNS-rebindcontrole"):
|
||||||
|
fetch_url("https://example.org/start", client=client)
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
class _PeerStream:
|
||||||
|
def __init__(self, address: str) -> None:
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
def get_extra_info(self, key: str):
|
||||||
|
return (self.address, 443) if key == "server_addr" else None
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetcher_accepts_public_cdn_rotation_when_connected_peer_is_public(monkeypatch):
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
def validate(url: str, **kwargs):
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
address = "150.171.109.35" if calls == 1 else "150.171.109.36"
|
||||||
|
return ValidatedUrl(url=url, hostname="cdn.example.org", port=443, addresses=(address,))
|
||||||
|
|
||||||
monkeypatch.setattr("apps.sources.services.fetcher.validate_public_url", validate)
|
monkeypatch.setattr("apps.sources.services.fetcher.validate_public_url", validate)
|
||||||
|
|
||||||
def handler(request: httpx.Request) -> httpx.Response:
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
if request.url.path == "/start":
|
status_code = 302 if request.url.path == "/start" else 200
|
||||||
return httpx.Response(302, headers={"location": "/final"})
|
headers = {"location": "/jobs"} if status_code == 302 else {"content-type": "text/html"}
|
||||||
return httpx.Response(
|
return httpx.Response(
|
||||||
200,
|
status_code,
|
||||||
headers={"content-type": "text/html; charset=utf-8"},
|
headers=headers,
|
||||||
content=b"Vacature",
|
content=b"" if status_code == 302 else b"Vacature",
|
||||||
|
extensions={"network_stream": _PeerStream("150.171.109.36")},
|
||||||
)
|
)
|
||||||
|
|
||||||
client = httpx.Client(transport=httpx.MockTransport(handler))
|
client = httpx.Client(transport=httpx.MockTransport(handler))
|
||||||
with pytest.raises(FetchError, match="DNS-rebindcontrole"):
|
|
||||||
fetch_url("https://example.org/start", client=client)
|
document = fetch_url("https://cdn.example.org/start", client=client)
|
||||||
|
|
||||||
|
assert document.status_code == 200
|
||||||
|
assert document.final_url == "https://cdn.example.org/jobs"
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetcher_rejects_private_connected_peer(monkeypatch):
|
||||||
|
_patch_dns(monkeypatch)
|
||||||
|
client = httpx.Client(
|
||||||
|
transport=httpx.MockTransport(
|
||||||
|
lambda request: httpx.Response(
|
||||||
|
200,
|
||||||
|
headers={"content-type": "text/html"},
|
||||||
|
content=b"Vacature",
|
||||||
|
extensions={"network_stream": _PeerStream("10.0.0.5")},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(FetchError, match="Niet-publiek verbonden IP-adres"):
|
||||||
|
fetch_url("https://example.org/jobs", client=client)
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user