This commit is contained in:
2026-04-20 15:41:13 +02:00
parent d0c16e6497
commit 1f66388064
159 changed files with 17590 additions and 0 deletions
@@ -0,0 +1,205 @@
# ČPZP — Automatické stahování zpráv
## Co to dělá
Dva skripty které se přihlásí na portál ČPZP a stáhnou všechny zprávy ze schránek:
1. `01_prihlaseni.py` — přihlásí se certifikátem, uloží cookies do `cpzp_cookies.json`
2. `02_stahuj_vse.py` — použije cookies, projde schránky, stáhne soubory do `Staženo/`
---
## Jak funguje přihlášení
Portál **nepoužívá heslo** — autentizuje certifikátem přes 3 kroky:
### Krok 1 — Získej session a challenge
```
GET https://portal.cpzp.cz/app/login/
→ server nastaví cookie: PHPSESSID=...
→ v HTML stránce je vložen JS objekt s challengem a CSRF tokenem
```
Challenge je v HTML jako JS výraz (ne JSON):
```javascript
CPZP = {
settings : {
certificateLoginKey : 'Prohlášení:'+ String.fromCharCode(13, 10) +
'Tímto se přihlašuji k Portálu ČPZP'+ String.fromCharCode(13, 10) +
''+ String.fromCharCode(13, 10) +
'Okamžik vygenerování tohoto prohlášení: 20.04.2026 14:52:47',
...
}
}
```
CSRF token je v HTML formuláři:
```html
<form name="frmPrihlasCert" method="post" action="/app/">
<input name="csrfCert" type="hidden" value="42ead46f7d374805c5d9...">
<input id="sign" name="sign" type="hidden" value="">
</form>
```
### Krok 2 — Podpis certifikátem
Challenge (sestavená z JS výrazu) se podepíše jako **PKCS7 / CMS SignedData**:
- Algoritmus: **RSA + SHA-256**
- Typ: **DetachedSignature** (obsah není vložen do podpisu)
- **BEZ CA řetězu** — pouze end-entity certifikát
- Výsledný formát: **PEM s hlavičkami** (`-----BEGIN PKCS7-----`)
```python
pem_podpis = (
pkcs7.PKCS7SignatureBuilder()
.set_data(challenge.encode("utf-8"))
.add_signer(cert, private_key, hashes.SHA256())
.sign(Encoding.PEM, [PKCS7Options.DetachedSignature])
)
```
> ⚠️ Na rozdíl od VoZP portál ČPZP očekává **celý PEM string včetně hlaviček**,
> ne jen base64 DER (přestože oboje přes NMSigner). Bez hlaviček vrátí 500.
### Krok 3 — Přihlášení
```
POST https://portal.cpzp.cz/app/
Content-Type: application/x-www-form-urlencoded
csrfCert=<token_z_formulare>&sign=<pem_podpis>
Úspěch: odpověď neobsahuje "frmPrihlasCert" (login stránka)
```
---
## Certifikát
| Položka | Hodnota |
|---|---|
| Soubor | `U:\ordinaceprojekt\Insurance\Certificates\MBQualifiedCert.pfx` |
| Vlastník | MUDr. Michaela Buzalková |
| Vydavatel | I.CA EU Qualified CA2/RSA 06/2022 |
| Platnost | do 2027-01-16 |
| Thumbprint | `056ED80A3CDDE31DD36EECE0181B4E78D61122A7` |
---
## Stahování souborů
`02_stahuj_vse.py` používá `requests` (bez Playwright) — portál nevyžaduje JS pro navigaci.
### Schránky které se prochází
| URL | Název |
|---|---|
| `/app/schranka/` | Schránka klienta |
| `/app/schranka-pzs/` | Schránka PZS |
> Obě schránky obsahují stejné zprávy — skript deduplicuje podle ID zprávy.
### Paginace
Portál zobrazuje 20 zpráv na stránku, stránkování přes `?offset=N`:
```
GET /app/schranka/?offset=0 → zprávy 120
GET /app/schranka/?offset=20 → zprávy 2140
...
```
Detekce konce: pokud stránka neobsahuje žádné nové ID, zastav.
### Struktura zprávy v seznamu
```html
<tr id="message-69cd7aa38ec68a837b5d8cbc" class="mail-read status-done">
<td>...</td>
<td>zpracováno</td>
<td>09305000 - MUDr. Michaela Buzalková</td>
<td>VYÚČTOVÁNÍ ZDRAVOTNÍ PÉČE Ref. č. 26274350</td>
<td>01.04.2026 22:05:42</td>
<td><a href="/app/schranka/detail/69cd7aa38ec68a837b5d8cbc/">zobrazit detail</a></td>
</tr>
```
ID zprávy je hex string (24 znaků), ne číslo jako u VoZP.
### Detail zprávy a stažení
```
GET /app/schranka/detail/{hex_id}/
→ stránka obsahuje odkaz na soubor
<a href="/app/schranka/protokol/?path=c387260ba8f041609a663038be79281c">
ZU250168094V1.pdf ← název souboru (nebo "26274350 (stáhnout protokol)")
</a>
```
```
GET /app/schranka/protokol/?path={hash}
→ vrátí soubor (PDF nebo HTML podle druhu zprávy)
```
### Typy souborů
| Druh zprávy | Typ souboru | Obsah |
|---|---|---|
| IČZ: ... Č.faktury: ... | **PDF** | Zúčtovací zpráva |
| VYÚČTOVÁNÍ ZDRAVOTNÍ PÉČE | **HTML** | Protokol přijetí vyúčtování |
| KLIENTELA | **HTML** | Protokol přijetí |
| Konečné vyúčtování | **PDF** | Závěrečné vyúčtování |
Přípona se určuje podle textu linku (`.pdf`) nebo Content-Type odpovědi.
### Pojmenování stažených souborů
```
YYYY-MM-DD Druh zprávy (Ref. XXXXXXXXX).přípona
```
Příklady:
```
2026-03-26 IČZ_ 09305000 Č.faktury_ 260027 (Ref. 26220064).pdf
2026-04-01 VYÚČTOVÁNÍ ZDRAVOTNÍ PÉČE (Ref. 26274350).html
2025-05-29 Konečné vyúčtování (Ref. 24774290).pdf
```
Znaky nepovolené ve Windows názvech (`/ : * ? " < > |`) se nahrazují podtržítkem.
---
## Rozdíly oproti VoZP
| | VoZP | ČPZP |
|---|---|---|
| Challenge zdroj | JSON API (`/json-api/prihlaseni/prihlasovaci-zprava`) | HTML stránka (JS výraz) |
| Challenge formát | JSON string s `\r\n` | JS string concatenation + `String.fromCharCode` |
| Podpis formát | PEM s hlavičkami | PEM s hlavičkami |
| Odeslání | POST JSON na API endpoint | POST form-data na `/app/` |
| CSRF | ne | ano (`csrfCert`) |
| ID zprávy | číselné | hex string (24 znaků) |
| Stahování | Playwright + `context.request.get()` | čisté `requests` |
| Typy souborů | vždy HTML | HTML i PDF |
---
## Závislosti
```
pip install requests cryptography beautifulsoup4
```
---
## Spuštění
```bash
python 01_prihlaseni.py # přihlásí, uloží cookies (nutné při expiraci session)
python 02_stahuj_vse.py # stáhne vše, přeskočí již existující soubory
```
Session cookie (`PHPSESSID`) expiruje — pokud `02_stahuj_vse.py` zahlásí
*"Cookies expirovala"*, spusť nejdřív `01_prihlaseni.py`.