1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:57:40 +03:00

TOC inside the docs

This commit is contained in:
Anton Afanasyeu
2026-06-03 06:23:08 +02:00
parent 4527e97dc0
commit bd339ee90c
24 changed files with 2768 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
After a **reset** or new session on deploy/infra work:
0. **[docs/README.md](docs/README.md)** — documentation index (linked TOCs for all guides in `docs/`).
1. Read **[docs/INFRA.md](docs/INFRA.md)** end-to-end (topology, **no :8089** for androidcast, BE `listen 80`, sshfs `tmp/` paths).
2. For hub deploy only: **[examples/app_hub/DEPLOY.md](examples/app_hub/DEPLOY.md)**.
3. For PHP/DB/tickets: **[examples/crash_reporter/backend/README.md](examples/crash_reporter/backend/README.md)**.

View File

@@ -0,0 +1,874 @@
# Android Cast — On-Demand Remote Access (Reverse Tunnel) Proposals
_Date: 2026-06-02 — **merged document**: initial proposal (rev. 1) + infra review addendum (rev. 2)_
---
## Table of contents
<!-- toc -->
- [Document history](#document-history)
- [Context and constraints](#context-and-constraints)
- [Executive summary](#executive-summary)
- [Initial proposal (rev. 1)](#initial-proposal-rev-1)
- [Refined after infra review (rev. 2)](#refined-after-infra-review-rev-2)
- [Stakeholder questions (rev. 2 — added to requirements)](#stakeholder-questions-rev-2-added-to-requirements)
- [Problem statement](#problem-statement)
- [Proposed control plane (heartbeat type: ra)](#proposed-control-plane-heartbeat-type-ra)
- [Device → BE (poll, random interval 17 min when enabled)](#device-be-poll-random-interval-17-min-when-enabled)
- [BE → device (only if whitelisted + operator requested session)](#be-device-only-if-whitelisted-operator-requested-session)
- [Disable (user unchecks box or admin revokes)](#disable-user-unchecks-box-or-admin-revokes)
- [Field reference](#field-reference)
- [State machine (simplified)](#state-machine-simplified)
- [Network topology — Gentoo FE → Alpine BE (rev. 2)](#network-topology-gentoo-fe-alpine-be-rev-2)
- [FE ↔ BE tuning (by track)](#fe-be-tuning-by-track)
- [Security model](#security-model)
- [Key handling options (rev. 1)](#key-handling-options-rev-1)
- [Embedding in the Android app (rev. 2)](#embedding-in-the-android-app-rev-2)
- [Backend integration ease — Alpine BE (rev. 2)](#backend-integration-ease-alpine-be-rev-2)
- [Licensing and open source (rev. 2)](#licensing-and-open-source-rev-2)
- [What “Custom file API” means (rev. 2 — clarified)](#what-custom-file-api-means-rev-2-clarified)
- [Tooling alternatives — full survey (rev. 1)](#tooling-alternatives-full-survey-rev-1)
- [Tooling summary — infra-focused (rev. 2)](#tooling-summary-infra-focused-rev-2)
- [Comparison matrix — initial scoring (rev. 1)](#comparison-matrix-initial-scoring-rev-1)
- [Comparison matrix — FE proxy focus (rev. 2)](#comparison-matrix-fe-proxy-focus-rev-2)
- [WireGuard vs SSH — decision lens (rev. 2)](#wireguard-vs-ssh-decision-lens-rev-2)
- [Recommendations](#recommendations)
- [Initial (rev. 1)](#initial-rev-1)
- [Refined (rev. 2 — Gentoo FE → Alpine BE)](#refined-rev-2-gentoo-fe-alpine-be)
- [RBAC roles (BE)](#rbac-roles-be)
- [BE admin UI (crashes look-and-feel)](#be-admin-ui-crashes-look-and-feel)
- [MariaDB schema (sketch — rev. 1)](#mariadb-schema-sketch-rev-1)
- [Android app changes (sketch — rev. 1)](#android-app-changes-sketch-rev-1)
- [Estimates (engineering)](#estimates-engineering)
- [Open questions (merged)](#open-questions-merged)
- [Rev. 3 — WG vs SSH Q&A + bastion deep-dive](#rev-3-wg-vs-ssh-qa-bastion-deep-dive)
- [Q1 — Exact FE/BE rules: WireGuard (DNAT) vs SSH (no DNAT required)](#q1-exact-febe-rules-wireguard-dnat-vs-ssh-no-dnat-required)
- [Q2 — Can we use ports other than :80? (heartbeats and everything else)](#q2-can-we-use-ports-other-than-80-heartbeats-and-everything-else)
- [Q3 — Why is “Proxyable (FE→BE)” low for WG (2) and high for SSH (5)?](#q3-why-is-proxyable-febe-low-for-wg-2-and-high-for-ssh-5)
- [Rev. 3 — Bastion Q&A](#rev-3-bastion-qa)
- [Q4 — How easy to implement? Command checklist (FE + BE)](#q4-how-easy-to-implement-command-checklist-fe-be)
- [Q5 — Bastion: file transfers only, or other operations too?](#q5-bastion-file-transfers-only-or-other-operations-too)
- [Source linkage](#source-linkage)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Document history
| Rev | Focus | Summary |
|---|---|---|
| **1** | Initial research memo | On-demand reverse tunnel via heartbeat `type: ra`; compare WG, SSH, tinc, Tailscale, etc.; hybrid **WireGuard outbound + SSH/SFTP bastion** as first pick. |
| **2** | Stakeholder Q&A | Gentoo FE → Alpine BE path, embed WG/SSH in app, licensing, Custom file API clarified; refined pick **reverse SSH/SFTP** (**Proxyable**) over WG (**Lightweight**) for this nginx topology. |
| **3** | WG vs SSH + bastion deep-dive | Exact FE/BE commands (DNAT vs `stream`), port policy, **Proxyable** scoring explained, bastion scope (files vs full debug). |
Both revisions are kept below. Where they differ, **§14 Recommendations** lists both and explains why rev. 2 refined the tunnel choice. **§Rev. 3** adds operator-ready command checklists for the final pick.
---
## Context and constraints
- **Need:** occasional remote debug / manual settings on **one field device** (e.g. edit `codecs.json` in app home, inspect logs) without a **persistent** device↔BE tunnel.
- **Stack:** minimalistic LAMP (nginx + php-fpm + MariaDB) on BE; Android app already posts to `/api/heartbeat.php`.
- **Traffic path (rev. 2):** **Mobile app → Gentoo FE** (TLS, firewall, nginx reverse proxy) **→ Alpine BE** (XEN HVM, project services). Heartbeat already follows this; any data tunnel must be tunable on **both** FE and BE.
- **Security:** no standing inbound connection from BE to device; user opt-in; admin whitelist; full audit trail; minimal secret exposure on wire.
- **Ops:** sessions expire on inactivity; 24h DB cleanup; user can disable instantly from app settings.
- **Convenience (rev. 2):** SCP-/SFTP-**like** file exchange is **not mandatory** but highly desirable for `codecs.json` and logs.
---
## Executive summary
### Initial proposal (rev. 1)
- **Recommended:** on-demand reverse tunnel orchestrated by BE heartbeat (`type: ra`) + **WireGuard or SSH over a single ephemeral port** on BE, with **per-session keys** and **PHP admin UI** matching crashes console look-and-feel.
- **Not recommended for v1:** always-on VPN mesh (tinc/Tailscale on every device), public inbound SSH to phones, or BE-initiated connections through carrier NAT.
- **Pattern:** device polls BE at **random 17 min** when user enables “Remote access”; BE responds only if device is **whitelisted**; device then **dials out** and attaches tunnel; operator connects to BE **bastion/port**, not to the phone directly.
- **Primary pick (rev. 1):** WireGuard outbound + SSH/SFTP on bastion for operator (combines mobile simplicity with familiar file edit workflow for `codecs.json`).
- **Fallback (rev. 1):** if UDP blocked on target networks, use **chisel/frp over HTTPS** on same `ra` vhost with identical control plane.
### Refined after infra review (rev. 2)
- **Control plane:** unchanged (heartbeat `type: ra`, checkbox, whitelist, jittered poll, device dials out).
- **Tunnel choice (refined):** prefer **reverse SSH (+ SFTP)** over WireGuard for v1 **on this stack**.
- **One word — SSH:** **Proxyable** — TCP (nginx **`stream`**) matches Gentoo FE → Alpine BE like existing HTTP proxy; SFTP gives SCP-like files.
- **One word — WireGuard:** **Lightweight** — excellent embeddable crypto, but **UDP does not ride nginx HTTP reverse-proxy**; FE needs UDP DNAT/firewall rules.
- **Custom file API:** optional third track — HTTPS upload/download via PHP (§10); good Phase-1 or narrow path without sshd.
---
## Stakeholder questions (rev. 2 — added to requirements)
| # | Question | Short answer |
|---|---|---|
| Q1 | Is WireGuard “best”? | Best **crypto/simplicity**, not best **FE→BE proxy fit** for your stack. |
| Q2 | Embed WG in app or separate app? | **Embed** via `wireguard-android` tunnel library + `VpnService`; no standalone WireGuard app. |
| Q3 | Embed SSH or separate app? | **Embed** Dropbear/libssh2/MINA SSHD; no Termux. |
| Q4 | BE integration: WG vs SSH? | **SSH easier** on Alpine (`openssh`, `authorized_keys`, `-R` port). WG needs `wg set` + **UDP** FE→BE. |
| Q5 | Traffic Mobile → Gentoo FE → Alpine BE? | Heartbeat: `proxy_pass :80`. SSH: nginx **`stream`**. WG: **UDP DNAT** (not nginx `location /`). |
| Q6 | Licensing / open source? | See §9; Tailscale server is main license caveat. |
| Q7 | What is “Custom file API”? | HTTPS file transfer via PHP — not SCP, not a VPN; see §10. |
| Q8 | SCP-like file exchange? | Native **SFTP (SSH)**; similar UX via Custom file API; WG needs SSH/SFTP on top for SCP-like UX. |
---
## Problem statement
| Aspect | Detail |
|---|---|
| Trigger | Single device in the field needs interactive access (debug, replace config, pull logs). |
| Anti-goals | No permanent VPN; no open listening port on mobile; no broad fleet exposure. |
| User consent | Checkbox in app settings (“allow remote access at my own risk”). |
| Admin gate | Device ID must be on BE **remote_access_devices** whitelist before any session is offered. |
| Observability | Every enable/disable/connect/disconnect/timeout logged with reason and actor. |
| Path (rev. 2) | Signalling via FE→BE :80; tunnel endpoint on FE forwarded to BE. |
---
## Proposed control plane (heartbeat `type: ra`)
Reuse existing heartbeat endpoint; add branch for remote access.
### Device → BE (poll, random interval 17 min when enabled)
```json
{
"heartbeat": {
"type": "ra",
"status": "enable",
"device_id": "<stable device uuid>",
"random": "<client session nonce / reconnect token>",
"app_version": "0.1.0",
"capabilities": ["ssh", "sftp", "files_app_home"]
}
}
```
_Also accepted (rev. 2): `"capabilities": ["ssh_reverse", "sftp", "files_app_home"]`._
### BE → device (only if whitelisted + operator requested session)
```json
{
"heartbeat": {
"type": "ra",
"action": "connect",
"session_id": "uuid",
"endpoint": "ra.apps.f0xx.org:443",
"tunnel": "wireguard|ssh|ssh_reverse",
"credentials": {
"mode": "ephemeral_pubkey|otp",
"username": "ra-<session_id>",
"host_key_fingerprint": "…",
"remote_forward": "127.0.0.1:<be_port>:127.0.0.1:8022"
},
"expires_at": 1717340000
}
}
```
_WireGuard branch (rev. 1): `"tunnel": "wireguard"` with peer keys in `credentials`. SSH branch (rev. 2): `"tunnel": "ssh_reverse"` as above._
### Disable (user unchecks box or admin revokes)
```json
{ "heartbeat": { "type": "ra", "status": "disable", "device_id": "...", "random": "..." } }
```
### Field reference
| Field | Purpose |
|---|---|
| `random` | Client nonce for matching pending BE session; rotation on reconnect. |
| `session_id` | DB primary key for audit + port/key binding. |
| `status` | `enable` / `disable` / `poll` (optional alias for enable). |
| `action` | BE command: `connect`, `wait`, `deny`, `rotate`. |
### State machine (simplified)
```text
[user enables RA] → device polls → BE: wait (not whitelisted / no operator)
→ admin opens session for device_id → next poll → BE: connect + creds
→ device opens outbound tunnel → BE marks active → operator uses bastion
[user disables] → disable heartbeat → tunnel tear-down → BE inactive + audit row
[24h idle] → BE cron purges stale rows + closes ports
```
---
## Network topology — Gentoo FE → Alpine BE (rev. 2)
```text
[Android app]
│ HTTPS POST /api/heartbeat.php (type: ra)
[Gentoo FE — apps.f0xx.org]
│ nginx: proxy_pass http://artc0.intra.raptor.org:80 (existing)
[Alpine BE — artc0 / XEN HVM]
│ php-fpm + MariaDB (control plane)
│ sshd / ra-bastion (data plane — SSH track)
│ wg0 + UDP listener (data plane — WG track, if used)
[Operator laptop]
│ SSH/SFTP to FE or BE bastion port (never to phone IP)
session forwarded to device's reverse tunnel
```
### FE ↔ BE tuning (by track)
| Track | Public entry (FE) | FE config | BE service | Notes |
|---|---|---|---|---|
| **Heartbeat (all)** | `https://apps.f0xx.org/.../heartbeat.php` | `proxy_pass http://artc0:80` | php-fpm | Already works. |
| **Reverse SSH** | `ra.apps.f0xx.org:443` TCP or `:2222` | `stream { proxy_pass artc0:22; }` | `sshd` + `Match User ra-*` | Rev. 2 pick (**Proxyable**). |
| **WireGuard** | `ra.apps.f0xx.org:51820/udp` | **iptables/nft DNAT** → BE | `wg-quick` / dynamic peers | Rev. 1 pick candidate (**Lightweight**). |
| **Custom file API** | `https://apps.f0xx.org/.../api/ra/*` | existing HTTP proxy | php-fpm only | No stream/UDP rules. |
---
## Security model
| Layer | Mechanism |
|---|---|
| Authorization | RBAC roles (§12) + per-device whitelist + user opt-in on device. |
| Authentication | Per-session ephemeral key pair or one-time port + password; no long-lived device secrets in APK. |
| Transport | TLS to BE for heartbeat; tunnel encrypted (WireGuard / SSH). |
| Exposure | BE listens on dedicated **ra** vhost/port; no inbound to mobile; optional IP allowlist for operators. |
| Audit | `remote_access_events` table: who, device, action, reason, ip, timestamps. |
| Cleanup | Inactivity timeout (e.g. 3060 min); daily purge of records > 24h; credential invalidation on disable. |
| Reconnect | Same `session_id` + rotated `random` within grace window; new keys if expired. |
### Key handling options (rev. 1)
| Mode | Description | Risk |
|---|---|---|
| Ephemeral WG keypair | BE generates keys per session; device installs peer config once | Low; keys short-lived |
| SSH host cert + user cert | Step-CA or internal CA signs 1h user cert | Medium ops; mature tooling |
| Pre-allocated device key | One WG pubkey per device in DB | Higher leak impact; simpler reconnect |
| OTP + reverse SSH | `autossh -R random_port` with password from BE | Simple; weaker than pubkey |
**Key ceremony (rev. 1):** ephemeral WireGuard or SSH **user certificate** per session; optional device **identity key** (Ed25519) registered at whitelist time for authenticating poll requests only (not tunnel).
**Additional (rev. 2):**
- Terminate operator SSH on FE stream or require VPN to Gentoo before bastion.
- Device **outbound-only** to `ra.apps.f0xx.org:443` (TCP).
- Per-session **`ForceCommand internal-sftp`** if shell not required.
---
## Embedding in the Android app (rev. 2)
| Solution | Separate app required? | How to embed | Android notes |
|---|---|---|---|
| **WireGuard** | **No** | [`wireguard-android`](https://git.zx2c4.com/wireguard-android) tunnel AAR / `GoBackend` | `VpnService`; VPN icon while active. |
| **SSH reverse** | **No** | Dropbear + JNI, **libssh2**, **Apache MINA SSHD** | Foreground service; no VPN icon. |
| **frp/chisel client** | **No** | Static binary or Go mobile bind | Extra artifact. |
| **Custom file API** | **No** | OkHttp + JSON | Pure Java/Kotlin. |
You do **not** need Termux or the standalone WireGuard app if libraries are embedded in **Android Cast**.
---
## Backend integration ease — Alpine BE (rev. 2)
| Aspect | Reverse SSH | WireGuard |
|---|---|---|
| Package on Alpine | `openssh` | `wireguard-tools` + kernel mod |
| Session open (PHP) | `authorized_keys` + `-R` port; optional `ForceCommand internal-sftp` | `wg set wg0 peer …` |
| Session close | Remove key; kill session; free port | `wg set … remove` |
| FE cooperation | nginx **`stream`** (TCP) | **UDP DNAT** |
| Operator access | `sftp -P 443 ra-...@ra.apps.f0xx.org` | `wg-quick up` or SSH over WG |
| Jump host `f0xx.org:222` | Natural extension | Separate UDP path |
---
## Licensing and open source (rev. 2)
| Component | License | Ship in app / BE | Notes |
|---|---|---|---|
| **WireGuard** (kernel module) | **GPL-2.0** | BE kernel | Stock Alpine package. |
| **wireguard-go** / Android tunnel | **MIT** / **Apache-2.0** | App embed | Userspace embed. |
| **OpenSSH** | **BSD-style** | BE + client | Permissive. |
| **Dropbear** | **Permissive (MIT-like)** | App embed | Small client. |
| **libssh2** | **BSD-3-Clause** | App embed | |
| **Apache MINA SSHD** | **Apache-2.0** | App (Java) | SSH/SFTP in Java. |
| **chisel** | **MIT** | App/BE | |
| **frp** | **Apache-2.0** | App/BE | |
| **tinc** | **GPL-2.0+** | — | Copyleft; mesh. |
| **Tailscale client** | **BSD-3-Clause** | — | Client OSS. |
| **Headscale / Tailscale server** | **BSD** / **BSL** | — | Self-host Headscale if needed. |
| **Custom file API** | **Project code** | BE PHP | No third-party tunnel license. |
_GPL-2.0 Android app: use permissively licensed clients (MIT/BSD/Apache); review before linking GPL client libs into APK._
---
## What “Custom file API” means (rev. 2 — clarified)
**Not** a tunnel and **not** the SCP protocol. Application-layer remote file exchange on existing PHP:
- Endpoints: `POST /api/ra/upload`, `GET /api/ra/download`, `GET /api/ra/list`.
- Auth: short-lived token from heartbeat `connect` (same session as control plane).
- Scope: **`getFilesDir()`** / `codecs.json` unless Storage permission granted (broader scope needs second consent).
- Operator: crashes-themed **web UI** or `curl`**SCP-like** push/pull without `scp` binary.
- **Pros:** no FE stream/UDP; minimal attack surface (no shell); reuses LAMP.
- **Cons:** no shell; implement listing/chunking for large logs.
**Use when:** Phase-1 before sshd bastion, or permanent narrow path alongside SSH for power users.
_Original rev. 1 one-liner: “Custom HTTPS file API — PHP endpoints; no shell; minimal attack surface; more app code for file ops.”_
---
## Tooling alternatives — full survey (rev. 1)
| Option | Maturity | Mobile fit | BE fit | Pros | Cons / risks |
|---|---|---|---|---|---|
| **WireGuard (outbound from device)** | High | Excellent (userspace WG) | Single UDP listener / per-session peers | Fast, modern crypto, small code | UDP blocked on some carriers; peer lifecycle automation |
| **Reverse SSH (`autossh -R`)** | High | Good (embedded dropbear client) | sshd on bastion | Familiar, SFTP, shell | Key mgmt; battery if misconfigured |
| **SSH via WebSocket (chisel, frp)** | Medium | Good | One HTTPS port | Traverses strict firewalls | Extra binary; audit frp history |
| **OpenVPN / IKEv2 (on-demand)** | High | Heavy | openvpn-access | Full tunnel | Overkill; battery; not file-focused |
| **tinc** | Medium | Poor (daemon, mesh) | mesh coordinator | P2P mesh | Persistent mesh ≠ on-demand; key distribution painful |
| **Tailscale / Headscale** | High | App + VPN | control server | Easy UX | Persistent identity; fleet-wide trust |
| **MeshCentral / RustDesk** | High | Agent app | relay server | Remote desktop | Different UX; screen control not primary need |
| **ADB over TLS tunnel** | Medium | Dev-only | reverse port | Exact Android debug | Dev options; security sensitive |
| **Custom HTTPS file API** | Low (build) | Native | PHP endpoints | No shell; minimal attack surface | No arbitrary shell; more app code |
---
## Tooling summary — infra-focused (rev. 2)
| Option | Embed? | FE→BE fit | SCP-like | License | Verdict |
|---|---|---|---|---|---|
| **Reverse SSH + SFTP** | Yes | **Excellent** (`stream`) | **Native** | BSD | **v1 pick (rev. 2)** |
| **WireGuard outbound** | Yes | Fair (UDP DNAT) | Via SFTP over WG | MIT/GPL | **v1 pick (rev. 1)** / v2 if UDP path ready |
| **Custom file API** | Yes | **Excellent** (HTTP) | Similar UX | Yours | Phase-1 / narrow |
| **frp/chisel** | Yes | Good | Possible | MIT/Apache | Fallback (both revs) |
| **tinc / Tailscale** | VPN app | Poor on-demand | Varies | GPL/BSL | Not recommended |
---
## Comparison matrix — initial scoring (rev. 1)
Scoring: 1 = low, 5 = high. Lower total was interpreted as stronger general fit in rev. 1 table orientation.
| Criterion | WG outbound | Reverse SSH | frp/chisel | tinc | Tailscale | Custom file API |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| Security | 5 | 4 | 3 | 3 | 4 | 5 |
| Simplicity (ops) | 4 | 4 | 3 | 2 | 3 | 4 |
| On-demand fit | 5 | 5 | 5 | 1 | 2 | 5 |
| File access (app dir) | 4 | 5 | 4 | 3 | 4 | 5 |
| Shell/debug | 4 | 5 | 4 | 3 | 4 | 1 |
| Fits LAMP BE | 4 | 5 | 4 | 3 | 3 | 5 |
| Battery / data | 4 | 3 | 3 | 2 | 2 | 5 |
| **Weighted total** | **31** | **30** | **26** | **17** | **22** | **30** |
_Rev. 1 tie note: WireGuard, reverse SSH, and Custom file API scored highest. Rev. 1 suggested **WireGuard + SFTP/SSH bastion** if shell needed; **Custom file API** if only `codecs.json` + logs._
---
## Comparison matrix — FE proxy focus (rev. 2)
| Criterion | Rev SSH | WG outbound | Custom file API | frp/chisel |
|---|:---:|:---:|:---:|:---:|
| **Proxyable (FE→BE)** | **5** | 2 | **5** | 4 |
| Security | 4 | 5 | 5 | 3 |
| On-demand fit | 5 | 5 | 5 | 5 |
| SCP-like files | **5** | 3* | 4 | 3 |
| BE integration ease | **5** | 3 | **5** | 3 |
| Embed without 3rd-party app | 5 | 5 | **5** | 4 |
| **Total** | **29** | 23 | **29** | 22 |
\*WG files row assumes SSH/SFTP layered on tunnel.
---
## WireGuard vs SSH — decision lens (rev. 2)
| Criterion | WireGuard | Reverse SSH + SFTP |
|---|---|---|
| **One-word headline** | **Lightweight** | **Proxyable** |
| Embed in app | Yes (`VpnService`) | Yes (libssh2/MINA/dropbear) |
| FE nginx HTTP proxy | No (UDP) | Yes (**`stream` TCP**) |
| Gentoo → Alpine path | Firewall UDP DNAT | Same as HTTP ops model |
| SCP-like files | Needs SSH/SFTP on top | **Built-in (SFTP)** |
| Carrier networks | UDP sometimes blocked | TCP 443 usually works |
---
## Recommendations
### Initial (rev. 1)
1. **Phase 0** — Design + threat model (12 days): consent copy, port allocation, key ceremony.
2. **Phase 1** — Control plane (12 weeks): heartbeat `ra`, whitelist, admin UI, audit, cron; dry-run `wait`/`connect`.
3. **Phase 2** — Tunnel (12 weeks): **WireGuard outbound** from app OR **reverse SSH to bastion**; operator connects via existing VPN/SSH to BE network.
4. **Phase 3** — Hardening: rate limits, mTLS on heartbeat, device identity signing, reconnect, pen-test.
**Primary pick (rev. 1):** WireGuard outbound + SSH/SFTP on bastion for operator.
**Fallback (rev. 1):** chisel/frp over HTTPS on same `ra` vhost.
### Refined (rev. 2 — Gentoo FE → Alpine BE)
**Primary v1:** **reverse SSH (+ SFTP)** — path is Mobile → Gentoo FE (nginx) → Alpine BE; **Proxyable** TCP; SFTP for `codecs.json`.
**Secondary:** WireGuard when dedicated **UDP forwarding** on FE is acceptable — **Lightweight** tunnel.
**Optional parallel:** **Custom file API** for whitelist-only file push/pull without internet-facing sshd.
**Phases (merged):**
1. Phase 0 — FE/BE port map + threat model (12 d).
2. Phase 1 — heartbeat `ra` + admin UI + optional Custom file API (12 w).
3. Phase 2 — reverse SSH/SFTP + Gentoo `stream` → Alpine sshd **or** WG track if chosen (12 w).
4. Phase 3 — MFA, reconnect, pen-test.
---
## RBAC roles (BE)
| Role | Permissions |
|---|---|
| **remote_access_user** | Open/close own sessions for whitelisted devices; view active/inactive list (scoped); SFTP / file API. |
| **admin** | Same as user + add/remove whitelist entries + assign `remote_access_user` role. |
| **root** | Full role management + force-disconnect any session + view all audit logs. |
Integrate with existing `users` / `Rbac`; new permissions: `remote_access_view`, `remote_access_operate`, `remote_access_admin`.
---
## BE admin UI (crashes look-and-feel)
- New nav: **Remote access** under `/app/androidcast_project/crashes/?view=remote_access` (or `/remote/`).
- Reuse `layout.php`, themes, locale picker, session auth.
- Screens (rev. 1):
1. **Devices** — whitelist, last seen, RA opt-in status (from last heartbeat).
2. **Sessions** — active / inactive; device, operator, started, last activity, tunnel type, port, status.
3. **Audit log** — filter by device, user, action, date.
4. **Open session** — pick device → pending session → waits for next device poll.
- Rev. 2 add-on: **file browser** tab when Custom file API enabled (upload/download `codecs.json`).
- No new frontend framework; same CSS/JS as tickets/graphs.
---
## MariaDB schema (sketch — rev. 1)
```sql
-- remote_access_devices (whitelist)
-- remote_access_sessions (session_id, device_id, operator_user_id, status,
-- tunnel, port, keys_ref, expires_at, last_activity)
-- remote_access_events (audit)
-- remote_access_credentials (optional encrypted blob, TTL)
```
Cron: `scripts/purge_remote_access.php` — inactive > 24h → `closed_timeout`; revoke keys; free ports.
---
## Android app changes (sketch — rev. 1)
- Settings → Developer → **Allow remote access** (off by default, strong warning).
- `RemoteAccessService` (foreground while tunnel active): jittered alarm 17 min → POST heartbeat.
- On `action: connect`: start WireGuard/SSH client with supplied creds; expose app files dir only unless Storage permission granted (broader scope needs second consent).
- On disable: stop service, wipe session keys, POST `status: disable`.
- Persist nothing long-term except optional device identity pubkey for poll authentication.
---
## Estimates (engineering)
| Track | Initial delivery | Hardening | Total |
|---|---:|---:|---:|
| Control plane only (heartbeat + DB + admin list) | 58 d | 35 d | 813 d |
| Control plane + Custom file API | 69 d | 35 d | 914 d |
| + WireGuard outbound tunnel (rev. 1 pick) | 812 d | 58 d | 1320 d (+ FE UDP) |
| + Reverse SSH/SFTP bastion (rev. 2 pick) | 711 d | 47 d | 1118 d |
| + FE nginx `stream` + firewall docs | +12 d | +1 d | (included in SSH track) |
_Assumptions: one BE bastion; excludes Play Store policy review._
---
## Open questions (merged)
**Rev. 1:**
- Maximum concurrent RA sessions per BE?
- Store tunnel configs only in RAM vs encrypted MariaDB blob?
- Require operator MFA (TOTP) before `Open session`?
- Play Store disclosure for “remote access” foreground service?
**Rev. 2:**
- Public TCP port: dedicated `2222` vs share `443` with `ssl_preread`?
- SFTP-only vs full shell for `remote_access_user`?
- Store device identity Ed25519 pubkey at whitelist time?
- Rate limit heartbeat `ra` polls?
**Rev. 3:**
- Confirm public RA port(s) with FE firewall owner before Phase 2?
- SFTP-only (`ForceCommand internal-sftp`) vs full shell for v1?
---
## Rev. 3 — WG vs SSH Q&A + bastion deep-dive
_Added 2026-06-02 — concrete FE/BE commands and decision support._
Constants used below (from [INFRA.md](INFRA.md)):
| Symbol | Value |
|---|---|
| **FE** | Gentoo HVM, public `apps.f0xx.org` (TLS **443** terminates here) |
| **BE** | Alpine `artc0.intra.raptor.org` / `10.7.16.128`, app vhost **`listen 80`** |
| **FE→BE (existing HTTP)** | `proxy_pass http://artc0.intra.raptor.org:80` |
| **Operator jump (existing)** | `f0xx.org:222` → Alpine `:22` |
Replace `10.7.16.128` with the live BE IP if it changes.
---
### Q1 — Exact FE/BE rules: WireGuard (DNAT) vs SSH (no DNAT required)
**Short answer:** WireGuard **must** use **UDP DNAT** (or equivalent firewall forward) on Gentoo FE because nginx **`proxy_pass` is HTTP/TCP-only** and cannot carry WG packets. SSH **does not need DNAT** on FE if you use nginx **`stream`** TCP forwarding (same nginx daemon, different block). You *can* use TCP DNAT for SSH as an alternative, but it is redundant when `stream` works.
#### WireGuard track — FE (Gentoo): UDP DNAT + forward allow
Public UDP **51820** on FE → BE **51820**. Example with **nftables** (adjust interface `eth0`, table/chain names to match your FE):
```bash
# FE — one-time / boot-persistent (example)
BE_IP=10.7.16.128
WG_PORT=51820
nft add rule inet nat prerouting iifname "eth0" udp dport $WG_PORT \
dnat ip to ${BE_IP}:${WG_PORT}
nft add rule inet filter forward ip daddr ${BE_IP} udp dport ${WG_PORT} accept
```
**iptables equivalent:**
```bash
BE_IP=10.7.16.128
WG_PORT=51820
iptables -t nat -A PREROUTING -i eth0 -p udp --dport $WG_PORT \
-j DNAT --to-destination ${BE_IP}:${WG_PORT}
iptables -A FORWARD -p udp -d ${BE_IP} --dport $WG_PORT -j ACCEPT
```
**Persist:** save rules (`/etc/nftables.conf` or `iptables-save` → Gentoo `iptables` init). **Open UDP 51820** on FE edge firewall (in addition to 443/tcp).
**Verify from outside:**
```bash
# should show BE wg listening (once BE step done)
nc -u -v apps.f0xx.org 51820
```
#### WireGuard track — BE (Alpine): interface + listen
```bash
# BE
apk add wireguard-tools
modprobe wireguard 2>/dev/null || echo "wireguard module required"
cat >/etc/wireguard/wg0.conf <<'EOF'
[Interface]
Address = 10.66.66.1/24
ListenPort = 51820
PrivateKey = <BE_WG_PRIVATE_KEY>
# PostUp/PostDown: optional iptables for operator subnet
EOF
wg-quick up wg0
rc-update add wg-quick.wg0 default # if using OpenRC
```
**Per RA session** (PHP or admin script adds ephemeral peer — example):
```bash
wg set wg0 peer <DEVICE_EPHEMERAL_PUBKEY> allowed-ips 10.66.66.2/32
# on session end:
wg set wg0 peer <DEVICE_EPHEMERAL_PUBKEY> remove
```
Device connects **outbound UDP** to `apps.f0xx.org:51820` (DNAT lands on BE). Operator joins same WG net (second peer) or uses SSH **over** the tunnel (see Q5).
---
#### SSH track — FE (Gentoo): nginx **`stream`** (TCP proxy, not DNAT)
Preferred: public TCP **443** on `ra.apps.f0xx.org` (or dedicated **2222**) → BE **22**. No kernel DNAT if nginx terminates the TCP hop.
Add to FE nginx (often `/etc/nginx/nginx.conf`, **outside** `http {}`):
```nginx
stream {
upstream be_sshd {
server 10.7.16.128:22;
}
server {
listen 443; # or 2222 if 443 shared with TLS SNI split
proxy_pass be_sshd;
proxy_connect_timeout 30s;
}
}
```
If **443** is already used for HTTPS on the same IP, either:
- use **`listen 2222`** for RA SSH, or
- use **`ssl_preread`** on 443 to split HTTPS vs SSH by ClientHello (more complex).
**Reload FE nginx:**
```bash
nginx -t && nginx -s reload
```
**Verify:**
```bash
ssh -p 443 -o StrictHostKeyChecking=accept-new ra-test@ra.apps.f0xx.org
# or: nc -v ra.apps.f0xx.org 2222
```
#### SSH track — optional FE DNAT (parallel to WG, for comparison only)
If you refused nginx `stream`, SSH could mirror WG with **TCP DNAT**:
```bash
# FE — iptables example (TCP 2222 → BE:22)
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2222 \
-j DNAT --to-destination 10.7.16.128:22
iptables -A FORWARD -p tcp -d 10.7.16.128 --dport 22 -j ACCEPT
```
This works but **duplicates** what `stream` already does and bypasses nginx logging/timeouts. **Recommendation:** use **`stream`**, not DNAT, for SSH.
#### SSH track — BE (Alpine): sshd + reverse-forward slot
```bash
# BE
apk add openssh
rc-update add sshd default
rc-service sshd start
# Dedicated match block — /etc/ssh/sshd_config.d/ra.conf
cat >/etc/ssh/sshd_config.d/ra.conf <<'EOF'
Match User ra-*
AllowTcpForwarding yes
GatewayPorts no
X11Forwarding no
PermitTTY yes
# v1 file-only: ForceCommand internal-sftp
EOF
rc-service sshd restart
```
**Per session — device outbound reverse forward** (embedded client runs equivalent):
```bash
# On device (conceptual — app embeds this):
ssh -N -R 127.0.0.1:18022:127.0.0.1:8022 ra-<session_id>@ra.apps.f0xx.org -p 443
```
**BE — operator reaches device file area via forwarded port:**
```bash
# On BE (after device connected):
sftp -P 18022 ra-<session_id>@127.0.0.1
# or scp -P 18022 ...
```
---
#### Side-by-side: what runs where
| Step | WireGuard | Reverse SSH |
|---|---|---|
| **FE edge open** | UDP **51820** | TCP **443** or **2222** |
| **FE mechanism** | **nft/iptables DNAT** | nginx **`stream`** (or optional TCP DNAT) |
| **BE daemon** | `wg0` / `wg set` | `sshd` + ephemeral `Match User ra-*` |
| **Device direction** | Outbound UDP to FE public IP | Outbound TCP to FE → proxied to BE sshd |
| **Operator path** | WG peer IP or SSH over WG | SFTP/SSH to BE forwarded port (see Q5) |
---
### Q2 — Can we use ports other than `:80`? (heartbeats and everything else)
**Two different “ports” — do not conflate them.**
| Leg | Port today | Can change? | Notes |
|---|---|---|---|
| **Phone → FE** | **443** (HTTPS) | Yes, but **443 is correct** for TLS + carrier-friendly egress | App already targets `https://apps.f0xx.org/...`; users never hit `:80` directly. |
| **FE → BE (heartbeat, crashes, graphs)** | **80** (HTTP) | **Yes** | Requires new FE `proxy_pass` upstream **and** BE `listen` block. Not forbidden — just **unused today**. |
| **RA data plane (WG)** | **51820/udp** (proposed) | Must be **non-80** | UDP tunnel; unrelated to HTTP vhost. |
| **RA data plane (SSH)** | **443/tcp** or **2222/tcp** | Must be **non-80** | Raw TCP/`stream`; not HTTP `location /`. |
**Why heartbeats stay on the existing `:80` FE→BE path (recommended for v1):**
1. **Already deployed** — [nginx.fe-apps.f0xx.org.snippet](../examples/crash_reporter/backend/nginx.fe-apps.f0xx.org.snippet) and BE `listen 80` fragment are the repo source of truth; zero new FE/BE holes for control plane.
2. **Same security zone** as crash upload and graphs — one ops playbook.
3. **No benefit** to moving heartbeat to BE `:443` internally — the phone still speaks HTTPS to FE; internal hop is your choice.
4. **Port 8089** on BE is **Janus/other** — do **not** reuse for androidcast ([INFRA.md §2](INFRA.md)).
**If you intentionally want heartbeat on BE `:443` internally:**
```bash
# FE — change upstream in server block (example)
# proxy_pass https://artc0.intra.raptor.org:443;
# BE — ensure listen 443 ssl server includes /api/heartbeat.php location
```
That is a **separate migration**; not required for RA.
**Summary:** Public **443** for all HTTPS (including heartbeat). Internal FE→BE **80** for LAMP today — **can** change, **should not** for v1 unless you have another reason. RA tunnels **must** use **dedicated non-80** ports (UDP 51820 or TCP 443/2222 stream).
---
### Q3 — Why is “Proxyable (FE→BE)” low for WG (2) and high for SSH (5)?
The rev. 2 score measures one thing: **“Can we forward this service from Gentoo FE to Alpine BE the same way we already forward crashes/hub HTTP?”**
| | **Reverse SSH** | **WireGuard** |
|---|---|---|
| **Protocol** | TCP | UDP |
| **Existing FE tool** | nginx **`stream`** → `10.7.16.128:22` | **Not** nginx `location` / `proxy_pass` |
| **FE change type** | Add `stream {}` block; reload nginx | **Firewall DNAT** + forward rules; separate from nginx HTTP config in repo |
| **Ops familiarity** | Same team that edits `nginx.conf` | Often different (iptables/nft, UDP, kernel module) |
| **Debugging** | `nginx -t`, error log, `curl`/ `nc` TCP | `nft list ruleset`, UDP blocked on some carriers, `wg show` on BE |
| **Matches INFRA docs** | Extends nginx pattern | New parallel path not in current snippets |
**Score meaning:**
- **5 (SSH):** “Drop in a `stream` upstream to BE — same deployment rhythm as todays reverse proxy.”
- **2 (WG):** “Works, but **not** proxyable via HTTP nginx; needs **DNAT** (or a UDP proxy like `udp2raw`, which is extra complexity). Partial credit — not zero — because FE→BE forwarding is still one hop.”
**Important nuance:** Low **Proxyable** does **not** mean WG is insecure or bad on the phone — it means **your FE→BE operational model** favours TCP/nginx. That is why rev. 2 refined the v1 pick to SSH despite WG being **Lightweight** on-device.
**Custom file API also scores 5** because it reuses existing **`proxy_pass http://artc0:80`** with new PHP locations — no `stream`, no DNAT.
---
## Rev. 3 — Bastion Q&A
### Q4 — How easy to implement? Command checklist (FE + BE)
Rough effort (after control plane PHP/DB exists):
| Track | FE ops | BE ops | App embed | Typical first-time |
|---|---|---|---|---|
| **Reverse SSH + SFTP** | 1 nginx `stream` block + reload | `openssh`, `sshd_config.d/ra.conf`, session keys | libssh2/MINA client, `-R` forward | **~0.51 day** infra |
| **WireGuard outbound** | UDP DNAT + firewall + persist | `wireguard-tools`, `wg0`, dynamic peers | `wireguard-android` + `VpnService` | **~12 days** infra |
| **Custom file API only** | none (HTTP already proxied) | PHP endpoints only | OkHttp upload/download | **~0 FE days** |
#### SSH bastion — minimal ordered checklist
**FE (Gentoo):**
```bash
# 1. Edit nginx — add stream {} → 10.7.16.128:22 (see Q1)
nginx -t && nginx -s reload
# 2. Firewall — allow inbound TCP on chosen RA port (443 or 2222)
# (example — adjust for your firewall frontend)
iptables -A INPUT -p tcp --dport 2222 -j ACCEPT
```
**BE (Alpine):**
```bash
apk add openssh
mkdir -p /etc/ssh/sshd_config.d
# ra.conf Match User ra-* (see Q1)
rc-service sshd restart
# Per session (automate in PHP):
# echo "<PUBKEY>" >> /home/ra-<session_id>/.ssh/authorized_keys
# adduser -D -s /bin/sh ra-<session_id>
```
**App (Android):** foreground service → heartbeat poll → on `connect`, run embedded SSH client with `-R 127.0.0.1:<be_port>:127.0.0.1:8022` → local SFTP server or `internal-sftp` on device side (design choice).
**Operator:**
```bash
# Via existing jump, then BE localhost forward:
ssh -J f0xx.org:222 sftp -P 18022 ra-<session_id>@127.0.0.1
```
#### WireGuard — minimal ordered checklist
**FE (Gentoo):**
```bash
BE_IP=10.7.16.128
WG_PORT=51820
# nft DNAT rules (Q1) + persist + open UDP WG_PORT on edge
```
**BE (Alpine):**
```bash
apk add wireguard-tools
# wg0.conf + wg-quick up wg0
# per session: wg set wg0 peer ... allowed-ips ...
```
**App (Android):** `VpnService` + userspace WG → outbound to `apps.f0xx.org:51820`.
**Operator (files still need a channel):**
```bash
# Option A — add operator as second WG peer (PHP generates config)
# Option B — SSH/SFTP over WG inner IP (still need sshd on device or BE hop)
wg-quick up operator-ra-<session_id>
sftp user@10.66.66.2
```
WG **alone** does not give SFTP; you almost always add **SSH on top** or fall back to **Custom file API** — extra moving parts vs SSH-only track.
---
### Q5 — Bastion: file transfers only, or other operations too?
| Capability | Custom file API | SSH/SFTP bastion | WireGuard alone | WG + SSH on tunnel |
|---|---|---|---|---|
| Edit `codecs.json` | Yes (scoped paths) | Yes (SFTP) | No | Yes |
| Pull log files | Yes | Yes | No | Yes |
| Interactive shell (`logcat`, `run-as`, debug cmds) | **No** | **Yes** (if not `ForceCommand internal-sftp`) | No | Yes |
| Port forwarding (ADB-like / local tools) | No | **Yes** (`-L`/`-R`) | Yes (L3) | Yes |
| Packet capture / low-level network | No | Limited | Yes (L3 VPN) | Yes |
**What “bastion” means in this project:**
1. **Narrow bastion (file-only v1):** `ForceCommand internal-sftp`, chroot or path jail to app home — enough for **codecs.json + logs** with smallest attack surface. Matches your stated primary need.
2. **Full debug bastion:** interactive shell + reverse forwards — for **logcat**, running one-off commands, inspecting `/data/data/...` (where policy allows), or forwarding a port to a laptop tool. Same SSH track, one config flag difference.
3. **WireGuard “bastion”:** really a **VPN endpoint**, not a file bastion — gives IP reachability; you still mount **SFTP/SSH or file API** for codecs.json unless you build a custom agent.
**Practical recommendation for your decision:**
| Your priority | Best fit |
|---|---|
| **Only** replace `codecs.json` + download logs | **Custom file API** or **SFTP-only SSH** — bastion = file transfer only |
| Occasional **shell debug** on one field device | **Reverse SSH bastion** with shell allowed (not SFTP-only) |
| Lowest FE friction + familiar ops | **Reverse SSH** (`stream` to BE) — scores **Proxyable 5** |
| Minimum bytes on wire / best battery on tunnel | **WireGuard** — but budget **+SSH or file API** for files |
**Rev. 3 decision hint:** If bastion = **files only**, Custom file API + existing `:80` heartbeat is the shortest path (no FE `stream`, no UDP DNAT). If bastion = **files + shell debug**, **reverse SSH** wins on infra fit. Pure **WG** only wins if you explicitly want L3 VPN and accept FE UDP DNAT + a second protocol for files.
---
## Source linkage
- Editable source: `docs/20260602_REVERSE_SSH_proposals_summary.md`
- PDF: `docs/20260602_REVERSE_SSH_proposals_summary.pdf`
- Regenerate: `bash scripts/build-reverse-ssh-proposals-pdf.sh`

View File

@@ -0,0 +1,829 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 48 0 R /F5 50 0 R /F6 51 0 R
/F7 54 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 52 0 R /Fit ] /Rect [ 45.68504 694.2874 107.925 705.2874 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 52 0 R /Fit ] /Rect [ 45.68504 683.2874 129.725 694.2874 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 52 0 R /Fit ] /Rect [ 45.68504 672.2874 115.477 683.2874 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 52 0 R /Fit ] /Rect [ 59.68504 661.8499 133.875 671.8499 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 52 0 R /Fit ] /Rect [ 59.68504 651.8499 168.885 661.8499 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 52 0 R /Fit ] /Rect [ 45.68504 641.2874 244.885 652.2874 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 53 0 R /Fit ] /Rect [ 45.68504 630.2874 112.821 641.2874 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 53 0 R /Fit ] /Rect [ 45.68504 619.2874 198.205 630.2874 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 53 0 R /Fit ] /Rect [ 59.68504 608.8499 256.3275 618.8499 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 53 0 R /Fit ] /Rect [ 59.68504 598.8499 266.13 608.8499 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 53 0 R /Fit ] /Rect [ 59.68504 588.8499 213.9 598.8499 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 53 0 R /Fit ] /Rect [ 59.68504 578.8499 109.7025 588.8499 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 55 0 R /Fit ] /Rect [ 59.68504 568.8499 145.9575 578.8499 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 55 0 R /Fit ] /Rect [ 45.68504 558.2874 233.653 569.2874 ] /Subtype /Link /Type /Annot
>>
endobj
19 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 55 0 R /Fit ] /Rect [ 59.68504 547.8499 147.1125 557.8499 ] /Subtype /Link /Type /Annot
>>
endobj
20 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 55 0 R /Fit ] /Rect [ 45.68504 537.2874 98.58904 548.2874 ] /Subtype /Link /Type /Annot
>>
endobj
21 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 55 0 R /Fit ] /Rect [ 59.68504 526.8499 155.145 536.8499 ] /Subtype /Link /Type /Annot
>>
endobj
22 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 55 0 R /Fit ] /Rect [ 45.68504 516.2874 180.861 527.2874 ] /Subtype /Link /Type /Annot
>>
endobj
23 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 56 0 R /Fit ] /Rect [ 45.68504 505.2874 211.093 516.2874 ] /Subtype /Link /Type /Annot
>>
endobj
24 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 56 0 R /Fit ] /Rect [ 45.68504 494.2874 168.853 505.2874 ] /Subtype /Link /Type /Annot
>>
endobj
25 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 56 0 R /Fit ] /Rect [ 45.68504 483.2874 220.837 494.2874 ] /Subtype /Link /Type /Annot
>>
endobj
26 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 56 0 R /Fit ] /Rect [ 45.68504 472.2874 191.061 483.2874 ] /Subtype /Link /Type /Annot
>>
endobj
27 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 57 0 R /Fit ] /Rect [ 45.68504 461.2874 193.277 472.2874 ] /Subtype /Link /Type /Annot
>>
endobj
28 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 57 0 R /Fit ] /Rect [ 45.68504 450.2874 199.045 461.2874 ] /Subtype /Link /Type /Annot
>>
endobj
29 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 57 0 R /Fit ] /Rect [ 45.68504 439.2874 206.165 450.2874 ] /Subtype /Link /Type /Annot
>>
endobj
30 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 57 0 R /Fit ] /Rect [ 45.68504 428.2874 199.501 439.2874 ] /Subtype /Link /Type /Annot
>>
endobj
31 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 45.68504 417.2874 112.373 428.2874 ] /Subtype /Link /Type /Annot
>>
endobj
32 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 59.68504 406.8499 103.0275 416.8499 ] /Subtype /Link /Type /Annot
>>
endobj
33 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 59.68504 396.8499 203.8125 406.8499 ] /Subtype /Link /Type /Annot
>>
endobj
34 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 45.68504 386.2874 105.693 397.2874 ] /Subtype /Link /Type /Annot
>>
endobj
35 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 45.68504 375.2874 174.613 386.2874 ] /Subtype /Link /Type /Annot
>>
endobj
36 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 45.68504 364.2874 170.157 375.2874 ] /Subtype /Link /Type /Annot
>>
endobj
37 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 45.68504 353.2874 184.413 364.2874 ] /Subtype /Link /Type /Annot
>>
endobj
38 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 58 0 R /Fit ] /Rect [ 45.68504 342.2874 130.157 353.2874 ] /Subtype /Link /Type /Annot
>>
endobj
39 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 59 0 R /Fit ] /Rect [ 45.68504 331.2874 136.389 342.2874 ] /Subtype /Link /Type /Annot
>>
endobj
40 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 59 0 R /Fit ] /Rect [ 45.68504 319.6874 215.309 329.2874 ] /Subtype /Link /Type /Annot
>>
endobj
41 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 59 0 R /Fit ] /Rect [ 59.68504 309.8499 304.74 319.8499 ] /Subtype /Link /Type /Annot
>>
endobj
42 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 60 0 R /Fit ] /Rect [ 59.68504 299.8499 301.89 309.8499 ] /Subtype /Link /Type /Annot
>>
endobj
43 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 61 0 R /Fit ] /Rect [ 59.68504 289.8499 303.3975 299.8499 ] /Subtype /Link /Type /Annot
>>
endobj
44 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 61 0 R /Fit ] /Rect [ 45.68504 278.6874 127.053 288.2874 ] /Subtype /Link /Type /Annot
>>
endobj
45 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 61 0 R /Fit ] /Rect [ 59.68504 268.8499 266.205 278.8499 ] /Subtype /Link /Type /Annot
>>
endobj
46 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 62 0 R /Fit ] /Rect [ 59.68504 258.8499 251.025 268.8499 ] /Subtype /Link /Type /Annot
>>
endobj
47 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 62 0 R /Fit ] /Rect [ 45.68504 248.2874 98.59704 259.2874 ] /Subtype /Link /Type /Annot
>>
endobj
48 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
49 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 13 0 R 14 0 R
15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R
25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R 33 0 R 34 0 R
35 0 R 36 0 R 37 0 R 38 0 R 39 0 R 40 0 R 41 0 R 42 0 R 43 0 R 44 0 R
45 0 R 46 0 R 47 0 R ] /Contents 110 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
50 0 obj
<<
/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
51 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
52 0 obj
<<
/Contents 111 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
53 0 obj
<<
/Contents 112 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
54 0 obj
<<
/BaseFont /ZapfDingbats /Name /F7 /Subtype /Type1 /Type /Font
>>
endobj
55 0 obj
<<
/Contents 113 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
56 0 obj
<<
/Contents 114 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
57 0 obj
<<
/Contents 115 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
58 0 obj
<<
/Contents 116 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
59 0 obj
<<
/Contents 117 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
60 0 obj
<<
/Contents 118 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
61 0 obj
<<
/Contents 119 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
62 0 obj
<<
/Contents 120 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 109 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
63 0 obj
<<
/Outlines 65 0 R /PageMode /UseNone /Pages 109 0 R /Type /Catalog
>>
endobj
64 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260603062149+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260603062149+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False
>>
endobj
65 0 obj
<<
/Count 50 /First 66 0 R /Last 108 0 R /Type /Outlines
>>
endobj
66 0 obj
<<
/Dest [ 52 0 R /Fit ] /Next 67 0 R /Parent 65 0 R /Title (Document history)
>>
endobj
67 0 obj
<<
/Dest [ 52 0 R /Fit ] /Next 68 0 R /Parent 65 0 R /Prev 66 0 R /Title (Context and constraints)
>>
endobj
68 0 obj
<<
/Count 2 /Dest [ 52 0 R /Fit ] /First 69 0 R /Last 70 0 R /Next 71 0 R /Parent 65 0 R
/Prev 67 0 R /Title (Executive summary)
>>
endobj
69 0 obj
<<
/Dest [ 52 0 R /Fit ] /Next 70 0 R /Parent 68 0 R /Title (Initial proposal \(rev. 1\))
>>
endobj
70 0 obj
<<
/Dest [ 52 0 R /Fit ] /Parent 68 0 R /Prev 69 0 R /Title (Refined after infra review \(rev. 2\))
>>
endobj
71 0 obj
<<
/Dest [ 52 0 R /Fit ] /Next 72 0 R /Parent 65 0 R /Prev 68 0 R /Title (Stakeholder questions \(rev. 2 \204 added to requirements\))
>>
endobj
72 0 obj
<<
/Dest [ 53 0 R /Fit ] /Next 73 0 R /Parent 65 0 R /Prev 71 0 R /Title (Problem statement)
>>
endobj
73 0 obj
<<
/Count 5 /Dest [ 53 0 R /Fit ] /First 74 0 R /Last 78 0 R /Next 79 0 R /Parent 65 0 R
/Prev 72 0 R /Title (Proposed control plane \(heartbeat type: ra\))
>>
endobj
74 0 obj
<<
/Dest [ 53 0 R /Fit ] /Next 75 0 R /Parent 73 0 R /Title (\376\377\000D\000e\000v\000i\000c\000e\000 !\222\000 \000B\000E\000 \000\(\000p\000o\000l\000l\000,\000 \000r\000a\000n\000d\000o\000m\000 \000i\000n\000t\000e\000r\000v\000a\000l\000 \0001 \023\0007\000 \000m\000i\000n\000 \000w\000h\000e\000n\000 \000e\000n\000a\000b\000l\000e\000d\000\))
>>
endobj
75 0 obj
<<
/Dest [ 53 0 R /Fit ] /Next 76 0 R /Parent 73 0 R /Prev 74 0 R /Title (\376\377\000B\000E\000 !\222\000 \000d\000e\000v\000i\000c\000e\000 \000\(\000o\000n\000l\000y\000 \000i\000f\000 \000w\000h\000i\000t\000e\000l\000i\000s\000t\000e\000d\000 \000+\000 \000o\000p\000e\000r\000a\000t\000o\000r\000 \000r\000e\000q\000u\000e\000s\000t\000e\000d\000 \000s\000e\000s\000s\000i\000o\000n\000\))
>>
endobj
76 0 obj
<<
/Dest [ 53 0 R /Fit ] /Next 77 0 R /Parent 73 0 R /Prev 75 0 R /Title (Disable \(user unchecks box or admin revokes\))
>>
endobj
77 0 obj
<<
/Dest [ 53 0 R /Fit ] /Next 78 0 R /Parent 73 0 R /Prev 76 0 R /Title (Field reference)
>>
endobj
78 0 obj
<<
/Dest [ 55 0 R /Fit ] /Parent 73 0 R /Prev 77 0 R /Title (State machine \(simplified\))
>>
endobj
79 0 obj
<<
/Count 1 /Dest [ 55 0 R /Fit ] /First 80 0 R /Last 80 0 R /Next 81 0 R /Parent 65 0 R
/Prev 73 0 R /Title (\376\377\000N\000e\000t\000w\000o\000r\000k\000 \000t\000o\000p\000o\000l\000o\000g\000y\000 \024\000 \000G\000e\000n\000t\000o\000o\000 \000F\000E\000 !\222\000 \000A\000l\000p\000i\000n\000e\000 \000B\000E\000 \000\(\000r\000e\000v\000.\000 \0002\000\))
>>
endobj
80 0 obj
<<
/Dest [ 55 0 R /Fit ] /Parent 79 0 R /Title (\376\377\000F\000E\000 !\224\000 \000B\000E\000 \000t\000u\000n\000i\000n\000g\000 \000\(\000b\000y\000 \000t\000r\000a\000c\000k\000\))
>>
endobj
81 0 obj
<<
/Count 1 /Dest [ 55 0 R /Fit ] /First 82 0 R /Last 82 0 R /Next 83 0 R /Parent 65 0 R
/Prev 79 0 R /Title (Security model)
>>
endobj
82 0 obj
<<
/Dest [ 55 0 R /Fit ] /Parent 81 0 R /Title (Key handling options \(rev. 1\))
>>
endobj
83 0 obj
<<
/Dest [ 55 0 R /Fit ] /Next 84 0 R /Parent 65 0 R /Prev 81 0 R /Title (Embedding in the Android app \(rev. 2\))
>>
endobj
84 0 obj
<<
/Dest [ 56 0 R /Fit ] /Next 85 0 R /Parent 65 0 R /Prev 83 0 R /Title (Backend integration ease \204 Alpine BE \(rev. 2\))
>>
endobj
85 0 obj
<<
/Dest [ 56 0 R /Fit ] /Next 86 0 R /Parent 65 0 R /Prev 84 0 R /Title (Licensing and open source \(rev. 2\))
>>
endobj
86 0 obj
<<
/Dest [ 56 0 R /Fit ] /Next 87 0 R /Parent 65 0 R /Prev 85 0 R /Title (What \215Custom file API\216 means \(rev. 2 \204 clarified\))
>>
endobj
87 0 obj
<<
/Dest [ 56 0 R /Fit ] /Next 88 0 R /Parent 65 0 R /Prev 86 0 R /Title (Tooling alternatives \204 full survey \(rev. 1\))
>>
endobj
88 0 obj
<<
/Dest [ 57 0 R /Fit ] /Next 89 0 R /Parent 65 0 R /Prev 87 0 R /Title (Tooling summary \204 infra-focused \(rev. 2\))
>>
endobj
89 0 obj
<<
/Dest [ 57 0 R /Fit ] /Next 90 0 R /Parent 65 0 R /Prev 88 0 R /Title (Comparison matrix \204 initial scoring \(rev. 1\))
>>
endobj
90 0 obj
<<
/Dest [ 57 0 R /Fit ] /Next 91 0 R /Parent 65 0 R /Prev 89 0 R /Title (Comparison matrix \204 FE proxy focus \(rev. 2\))
>>
endobj
91 0 obj
<<
/Dest [ 57 0 R /Fit ] /Next 92 0 R /Parent 65 0 R /Prev 90 0 R /Title (WireGuard vs SSH \204 decision lens \(rev. 2\))
>>
endobj
92 0 obj
<<
/Count 2 /Dest [ 58 0 R /Fit ] /First 93 0 R /Last 94 0 R /Next 95 0 R /Parent 65 0 R
/Prev 91 0 R /Title (Recommendations)
>>
endobj
93 0 obj
<<
/Dest [ 58 0 R /Fit ] /Next 94 0 R /Parent 92 0 R /Title (Initial \(rev. 1\))
>>
endobj
94 0 obj
<<
/Dest [ 58 0 R /Fit ] /Parent 92 0 R /Prev 93 0 R /Title (\376\377\000R\000e\000f\000i\000n\000e\000d\000 \000\(\000r\000e\000v\000.\000 \0002\000 \024\000 \000G\000e\000n\000t\000o\000o\000 \000F\000E\000 !\222\000 \000A\000l\000p\000i\000n\000e\000 \000B\000E\000\))
>>
endobj
95 0 obj
<<
/Dest [ 58 0 R /Fit ] /Next 96 0 R /Parent 65 0 R /Prev 92 0 R /Title (RBAC roles \(BE\))
>>
endobj
96 0 obj
<<
/Dest [ 58 0 R /Fit ] /Next 97 0 R /Parent 65 0 R /Prev 95 0 R /Title (BE admin UI \(crashes look-and-feel\))
>>
endobj
97 0 obj
<<
/Dest [ 58 0 R /Fit ] /Next 98 0 R /Parent 65 0 R /Prev 96 0 R /Title (MariaDB schema \(sketch \204 rev. 1\))
>>
endobj
98 0 obj
<<
/Dest [ 58 0 R /Fit ] /Next 99 0 R /Parent 65 0 R /Prev 97 0 R /Title (Android app changes \(sketch \204 rev. 1\))
>>
endobj
99 0 obj
<<
/Dest [ 58 0 R /Fit ] /Next 100 0 R /Parent 65 0 R /Prev 98 0 R /Title (Estimates \(engineering\))
>>
endobj
100 0 obj
<<
/Dest [ 59 0 R /Fit ] /Next 101 0 R /Parent 65 0 R /Prev 99 0 R /Title (Open questions \(merged\))
>>
endobj
101 0 obj
<<
/Count 3 /Dest [ 59 0 R /Fit ] /First 102 0 R /Last 104 0 R /Next 105 0 R /Parent 65 0 R
/Prev 100 0 R /Title (Rev. 3 \204 WG vs SSH Q&A + bastion deep-dive)
>>
endobj
102 0 obj
<<
/Dest [ 59 0 R /Fit ] /Next 103 0 R /Parent 101 0 R /Title (Q1 \204 Exact FE/BE rules: WireGuard \(DNAT\) vs SSH \(no DNAT required\))
>>
endobj
103 0 obj
<<
/Dest [ 60 0 R /Fit ] /Next 104 0 R /Parent 101 0 R /Prev 102 0 R /Title (Q2 \204 Can we use ports other than :80? \(heartbeats and everything else\))
>>
endobj
104 0 obj
<<
/Dest [ 61 0 R /Fit ] /Parent 101 0 R /Prev 103 0 R /Title (\376\377\000Q\0003\000 \024\000 \000W\000h\000y\000 \000i\000s\000 \034\000P\000r\000o\000x\000y\000a\000b\000l\000e\000 \000\(\000F\000E!\222\000B\000E\000\) \035\000 \000l\000o\000w\000 \000f\000o\000r\000 \000W\000G\000 \000\(\0002\000\)\000 \000a\000n\000d\000 \000h\000i\000g\000h\000 \000f\000o\000r\000 \000S\000S\000H\000 \000\(\0005\000\)\000?)
>>
endobj
105 0 obj
<<
/Count 2 /Dest [ 61 0 R /Fit ] /First 106 0 R /Last 107 0 R /Next 108 0 R /Parent 65 0 R
/Prev 101 0 R /Title (Rev. 3 \204 Bastion Q&A)
>>
endobj
106 0 obj
<<
/Dest [ 61 0 R /Fit ] /Next 107 0 R /Parent 105 0 R /Title (Q4 \204 How easy to implement? Command checklist \(FE + BE\))
>>
endobj
107 0 obj
<<
/Dest [ 62 0 R /Fit ] /Parent 105 0 R /Prev 106 0 R /Title (Q5 \204 Bastion: file transfers only, or other operations too?)
>>
endobj
108 0 obj
<<
/Dest [ 62 0 R /Fit ] /Parent 65 0 R /Prev 105 0 R /Title (Source linkage)
>>
endobj
109 0 obj
<<
/Count 11 /Kids [ 49 0 R 52 0 R 53 0 R 55 0 R 56 0 R 57 0 R 58 0 R 59 0 R 60 0 R 61 0 R
62 0 R ] /Type /Pages
>>
endobj
110 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2046
>>
stream
Gatm=>BALX'RnB33);VHgUO\>!V4[\Ks[E<[^iWs(M5F[/[-$qU6;%"J%nK/,U?)dOk#O"N,:Q3\UWcjJe2:OrLZ"Ka(+m`(-t>r4G>0i&f?+Pd<P-C?F5#68V:i@L'dM$1^N,MK&=K#5r^!,)FG&HZ`lZETV%BU&qQS?iGV0r@`T,#%E]e)V+(Y47KOG3"-SfU*uGg@45tr.<<4IHBp]I[L[Y0!PVn[AE60#l)'%`u;[Okb96SLS@PD[[BD#eb+3+>rpihMuBe[WVeM!LeYn2fDC45u07&\0\N4T<k`=%(,Uh2^tdRQ1D0H+O84F_j`AfRV;Se:Us.7818/+c"[c6UrAJ>j4l1fVa.EQ:-m-+aWUlgMCqAGmZ@2+^?EK+S((UPdekVZ@\-/a/NC#_rp$\2u>N+.PhBS_"h0S!nnq68#c[>c[l/Qjq.VjMu&*%]iAL/,`H.D1b`[XHXK@kob?bq%A-c1qqRL+qq@c>11R<!<(7\L=&0->@$&?:i1gcj^NdWGNhs+A0\+,[#8]-i<ui27;H#,dL65dMe/5_bu2;a0%mVkQsb$9R=pGn,rKsIZ+DkEV]<k=\"q*'*kKdAc:UqMM!&EoPcc]>*l)m.:Q#9/COo<3[ZtAMUm2Tg1@Tu2X!D:t"%m'W1&r;i7t$HH0mK'[,V90h;b]8>1R3nkID_uf]j.d/OOVB)iU4dqL".W,:sQi`rbrW;]/q0M[^u/0Mf:dF,o35Xh]m5L@+iR=_;m318VFh(qTr1]mRkKMEDoL<(]DdU?i:Pt)g!SkX%[hkf1W'>W(*UdNMRI4q$t?[F?:X_<&8ku@Gf*"G>`G=fnG]sX_Q:"'rGH`o9/3V"ZPE^&0icc+##._S2mEPg@pNoXC?='!2`FFShgTOR9oqq-Lm4_jCMmY:K>Z6Y,7Io7&HBU>4@O94!Ech%l&"@Ct[?0,YqSL3898=bqD5:e'/hN<%j1s2A+b,5aq&BcW9dXbHQ4ap!>:op^FFb/--VX5'K?._#aRSs$D;\/%pU*'WcHh9m.>dc#A0:-'VLlCL[OQGg\dH-q9)hN%_Wigp!M,5d4;Q&grglYX0.ah4cEYI:51VOC6-p9j=#o]g_euUGrt@XEe%>-iOtZ=6pK')3Yr,N%KU5'!8NX@g>9u=Sn/Bg:8t&BlRZs?s6u;3'.(kQP*sKrIpd'23lHu0_%[#a$SFGV`"#K:(><H*MR7Pf?!^J/Z-N?R?46$=R@$^C?k#%=h9#8`L4q(\XiYfGiQ%VU04/,'W+/`[B/fJfX`M#j[+.`F9fFshK\H=kcd/,piO7RCV8Is8%^_9+#nRoC8icB<l*>:k71_4o#d%PW#=J)>3)dn?*[]l30b2d^cBWk8a*VmXf'r[i[,`C`NUD$T@TnVLljcQJ*7D<q@(#Zd`^[G'>QWSPdI$WoY/)33EktO^V6B9>Z+QGOBRJ9!HtGp.GJ)SDF'M4CQj6W;4si-=Ir.9:rC$19]tO"C=!8alE*j";O\X4fZ^qlNNa%F$SS>,XIL%7j"h*MB'Qo?$KA2dI5a9#F.:W43HJ[9EO=EaIMLC)GS'X29N)J5"gN'eO&,)9I:4l&Id>jNE"KdX(+3.'[Q#[s7l[.MGAkRW9%QM]_&V:[>[so4J12mN9`IJ)loL?el*mGXAH/A%oXFmBBtY=(1,'SlL;'[o-^;Lh"6;AE&5k"i2uuPi"7Z0G+T/a[XAYAORa[+i/e-WE[:2nh#G/c8_kUI'Aa*]PDMZ;I'll4d]k<^ID96fMmZ=VC'7,i`&d#+T=j9Qc[<#_:g&L9/ooi[=\"#Ea`Vm-%E5:LZ[;qCOJ3KaIE&Lph<Nk)YfKJOj5Z29V\*QT,XO!t\0QGne,=t%n9qRgAYT&_3`6i8@,fW!NQg`2EX@n(]c"H>bndlUU^L=P@g2`o,-#jWee!WR+,M7gXBFqk'2Fc[[LfP&!iP[BpG2h-43s9VV(M4i$^)[)7]"Aqe@s2+\%ntLGC5N@eU9hLA\IDf=QNFc-6aC`]H3WLW_tZKd.9<ha;i/)F1.Q@WH!UiWJnKgg]mg'!!#ke~>endstream
endobj
111 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4117
>>
stream
Gatm>CN%re(B*)s6"#743jiN^r2`+6Ou@_>g.[r[VY_OPJsL(SN@\g83JmL^n%!aIQE436"IWt5MnGk2I@Y^VMja@FP(2:F@i$rPqd_G)B!VZIqF7+%^9Pl7S%mf^fuX0.(lei5T8gj#A)L&WR"r_LBGIfmA&)\lAsr;g)^JlL#rDj`2&TT*heL9Y54VggB=;Lb]-dN`fpuu@fNU92L&$@Crn1JRV=9j"h9B,VEV;5QIeKB/Tca.o@$-V7eG/(l9Sj",=a+f;4$6dscL?N@*Z=]7&UKph5/b0^Ru!R47dp3V56p(k@#W>/Lu,[NJ8L68VU[e:?krhoW*'meB%nbFG4Kp@4>g&jZF=Er?cQ&])n.mK)l@*Sf[.'"CDl9eFp8F4SB9+tNL;L>fTSLMs!FOUFs">d(q*ud=k@\E'3kA=RIl](Z)lB@^6ss8&qPI3rLM1O*LsJafI7?ILWY^s.d^3YQ<c:<#9.a4Qd!k28(K"p$OIIZE-<HI)?I2L6c0n3PF'Ah">/k-ac8A9XM-Q<M<*F9HHg5?0@)#tI0LtG4EX5r"[oJg>%O[?1G;`p$a/8">DomO%b\o`i&(.\(7:m]PHD-3RGYob-e:EPaQnQ.K#O35'ENRM3Eoq^GV4620AZL^bT]WkN=_$_cN6cepM@uW\W/YNoB59H]=n0k@Bgoc*B)r,TNrUp:+kj+`3(h+X6j0f?67*ACsRS,8poK7jY=<&kAl4R]dkErD"%)Hm$c20NdVdGp;"1M$X`.c7D=+e1rI=f-`bucY3oP.>um.!DI\,K<\j/t@R$"ZcMj723^9cgkB*8E7Ym:lICdX"GKR+6B+KaoLo0(9o-!Tk_(I?C`8ajQM=hjVk3(nKoMOhl%iLG&%L[YcK\?s'*VNoo@(_4d!l<0c^G7b+b$Xq\Rhi>p=?s6[.'..j\R"?$YA3t&X>8a<aep__#GNtoQbL`j6t5?P\_m>H.Xi*(Df%C:8B#&6()AP$n;5-*TRg?cT<g6KA.*I=>?EDLHb.+q5%m7o3BhL&jp;WB4o[ef[k[cAM4)\Xf]<cj@ouhDeq/]C!Cd1YO&tL#g.@$9*_^[W"CKa\hh?qY.X8:Q;/JBpqe/kde#lY(5UN(sJ<P9GjQ.4d80kDmZ;"%4>ASlkaNQTd]=3RfqJZ<3l]!X-8,WJ@=loD&k;V=?Ns/`imrC=7%+oFfa=KP0pV&OY"%u!"";0B:5JkJ=d:i!jB_T<qHf3rqXFZQR(G/[8fIc6Q_pfIcH1<&-rhG;[%W0Z3RWcH.m0tD+@R-4G3VsND@cMk]]9)P4+fcNme;U]Y]ng,U(P-rjf[#hu2t9LVRHH4=6Bt5WS=5i$+'F#8,KN9hFZC=n0'ju,KM\OUc/uAMjiP`:1j`JE=\=WU*l"p]Sj2%r6jPUZ/U9^ISGd"3ZNT=>$l5g%fP;9eiGEZ)JNVoJVcuaQ&."pN&sIf'ad%At)Pc8iR@"M>rF3R!]_d<iA+:6\n@1_;<WFOck'4KqS]ThJ6-kRks3;OBWpj=$iOuEE:qA8,'1iJ[>/hcZE7SMaG(X_@LquuT^cnh,PTf!:!gHY<1fmXfhjk'`Be\NAEAac,dM&cAId%J4a_%APP-*eKFRVRd!mqXk6mKl;qXFogL[cDLau9FGd'/%+>uq<egROIVRmT[`]jbit,D>70?JASDNM+[l/A#*3[aGqIO*b54f,?O3RZj`J0)Khkr>HfV)>>/0<`+&QXmt"eO9kjU!e<RN(RJ'D*dUG18J86i8=]u30)R#]<Rp_djS'")CHuWG'5cY]a,P&NaED3R0,AeD@TTCp]7s*mg35P)AjroZXT.8YUuDt:\7"-sBA[Sg7&7dH%t[PGH?(nZ;:YpGqf5J:]-*@2[Jt;n`P29G]gM,`,3R<>ObN+!>!DhH/$RCp63[_o3al`$KHim6*qMK>Y<bR=*9\X;K`g1,6,'oJct&#L(RH^iU-BMQ`j6JmLi>;qO1-ts#URqS2F\+q&(;-tYr;ef`,$1rPaf<iB!Gde2S]mW-1:UQYjpX`87?ruaO@"8oALXG@hcY0C$pb,hGkbs]I3AA-*TPL.b_Zm*kcU4pc[%_,dboI!Pf3DFFQi?JDgO=9psgEZ&]^"3Wt%_Pel9u&7AimAq9Xa+QHk!Jjtcb-\ODH3#]N:b)oC?7-KQU(!hSbgCMolU`er'jp1m6/PRP1Q[!OP4C$m*F`kr*`fplp?F7sLPNNnMW%D(9a:m9/$#2jpg*]#R+NeW)!-O6Wj"r\#qgGL^E$A/iI"FGr0Efi0>=F<JZURZiNAL6foAq=k7<uSMc/$t2.m"F-nB*CY0Eocc`0$Mb5Z3/Y&u1B$Ds(>^.7N7Oe^'4lTlFC$^T6e"eDa=sBpoatS5IacN&=dq24/?e<s7*`N=C"Cc;^&>ZII,c"3Q44NRlRj<i0,O\]t9:+(&fbKZEM<Juu)`"sOB$Q.Tamja@r^\dQoC7QD^jFk(dAhc2bg-D($@eMPL&8&!MZT^h4<'jD&?,_MP,]qRGUE0Bq(=jB8=cD&Q'ATs8G85[GWd0dfoG=ZimQeh:1!<uHcC6sc*)(W7#Xq5Z>ntG'Do3!$""q=mF^sk4Ndi+;ED"`*'ACir(KaMT4V6<UaL-Us!o=-h[6.8@;et%f=7ObVmic-9]1rkE*AS=-%EGj49K=dd1)IN-7,[Lct8a;8U&@'bmjQkFXF-3CWN->Y)o1>kd_]2^@0\O_E&;)f^;@iR'T]"njRDVgY!]6eg$%]h#XDt)DLXJ9BQ6;*i8^*nY2),gk>F6gZC`CV\k3G(L[IPctVqo<t"KlJ=:t)sg4-FOCD'SVU!i7sqqg&_rX4]P1in@a0n/8Hk1b^NVg(/S6Z7HEA<VX\2<;-bsf$i(3iSINK7b]cr%5>MTR!_4@0^$bl#UB?/j#*;2d_/.%!a%0Y9@1^U`:U?k)/tV9A40HMK&/g_BbpJ!RpFnmM"R4?72;\'Z=+7oP^P"Q^5)R)7r0D[Ng3!^%X?W\]@rJ-@JJR;rL9l1`"r\8qkpHl*2_HMk#Ck[DRi-.VS#Wj&k!8Bga2c>.DD!hW93ZMZR[Oem$CM1Q<O0KMT@>L'`k;!-/6M.YA;7dEfVhM8YWBjp)N%R:phjY4-RHr":53e9VH$M9hlR`TT\I!H@mY0;pa#%3P>f/,>C()o:fXg4+]=0eOsfsi!#27+6rh!]S\';ZOa\Ac1>6@Jgi$?$dnU^0i__%U"\*J*EH#tK[PY08RQHN=jlHO1sm4GU)MBM0ic^t'UY'tJN!9bbu+0Ln1Um+Qkg/P>a=u4g.qhPh2OscmC]6LF4E1+ALAqr.e5!DHP4Cr-<CJJ=oTkc0/NWGJn'e$5[g>t?IAaHAC8Lp05i0l[:tK#20nQh\:5>uYCiQ/m1dGeT`ik.Pj\mo,U]ik'M.Zb4S]iB+3?5tPmqs%@=ie!G06+KHL,ssJWN;/3<\k@MGO(pH$<?UQ.H@nO^V#)c0$J*GYef.Yo#)849)]#3b@W&IS*t9mJqONjHTFT"7=M')Z<@pg0LdOG3-*C=CY'c?`Yu;V(oF7mUFGNHt0('CPIRO>Go[CgUD1HnYt]gs(K?`FOB/[WBW/O2[\GJa#RH#m>?p@H)Mg=W[ps^o),&Kal([dmN4%KjI@jqX"L5h.d?(\N[hSh*QN%[Cin"#"7uslYF]j6iOZKK08e[F.[=<2"#Gl/Ia_#P4hYD9q5!1?&K]?*H5e%O1L%Ce.;sM"$H/mf26M?2[^]mi]%"pi\k7GHL)82\imcVTcRmX+(`^-m@Fsp5d%XP,-tcMh&2ht(A6RJSU1::iS?t7/oFgq?74%?5Wh;SOn2<EZWCWBl-O)[u6!d+9))X[(:60i#A3IB2bAt2D<0'Vgn72tL]I^JRM![%o;99*OOm54CaQ.O5qq%lig=&QD\\NA8e9^`Y\*jRhKA&O"lKM3_PPm0N99ib5GgD6T6S1PdH98'[.4J&3m:U.,">$RHN4IPDb;'\RYcc!YI"/bZHio8&r;R)l^O(Cd4@iN6G&OCMTT0cp;Z\*cZ8H9#()kbu()c"0K.lG,Fj/6X>Z&f(]SZi@[<G>Y;S@-NI+*&tW&!DKjX&o!jVdi\58rurKGs<^07eG7*oPMjmO/g>CUg.(4\tMpYrD[f=d]>,~>endstream
endobj
112 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3124
>>
stream
Gau0FHZ.Xu')t1g@LuoJGHXjhl=D9\*g.japTWCn2stg>o>.):g=B:;<CtI<f7*H5ML&KmV<i&Y<Yd\8Eea2\c+"[jpsIN>QTi4X'[H/(cj3=sTG`UfdpM$VHLg_sb:'KtU?\0R[S$%IA)OY7<d^Xd8Us3h%>O*q6mEJ;kr?mprSrao=K4MBT_ZeI=]T*rVItg%Hqh:@6:a>T\:;\1e5R>3aTT%NH-)Yb3fGX>@<I9)BFS&<,m-Z=++5tQ_=S8*r[:ls`)"KM1G)&'<TaNjV\*4<h'N!A(Tte$h;\?-YEI+cBk?>4V=9KK$0Q8O4)ET8(V0X"leB/r'M):G75Hb>bsDb:QU;L>Utc=6\\p1JcY/Oo(elXR@p+t*'Lr#bGokBnL5W<?5MJ_[qtp+(&8^.[.6gnE1eo5arOjMLh\DWBD",ou"E$D.9cLGn)[/>a`89NIR^bJ1o>\)h/*4!u^bc98O`fH8L3*=4?*YSrK+4=Q%p8lehaL2hj*h7%Ff$Xg8]J1Bdmk821BoXEfZkd:WU%MrMDcH\k^c1\`o'cS!&$JTK-bq2UF=Wk$3i(C2990rg;e=sSEo>i9nWp26M.b!38m'&Kniu/I`<P+LMh7JCJRZ?g%>5b3k:US,S:LF2<s_JV(d0Z&I5R,f^Sjoam8R,c-J*?fAsgql<R_NVKUWD;$ZE2HsFG(\Y9dNT)445C"^sU97oq\P9ki+dKfF3#M':tpNKP8l3O01lrCsh;4"HSH8L4L^Sgq3(dOd91k\L15S,WXMFI"q:H;kq8U(MV*^MDfZ/jC&D)_%sr;4Nk1+'G<C=%A-p?k='iA"&Q.ekb0HZ5#J7041``Chqff#Q<M(b+2'0j.R)n1C9J'O@-B[sS65De0"-jFh.1dOrHEMO.d"?+&a!MWs+HS,V'MGf*L^Z)JXL3K@S'8;R"iJA>&&]jonSo(fJ+c:EaJ+HjH?/SGdeKu\Rs':LTse*]LXc#JuIlfDH13[Qn&:!llN",^q.a!^qL7dQlm8^iV7THo<"/ZLP6e[E/Pn29U.I!,#akLK3"7AQlBFJ(ZhlN=pn2%Ep2+^6#%:SJNA;QUb?^0q<p`<PZ-&62Xj'1b3RF>k-S*LA)Xp]GQc4lQj.e1:6C#ot_)-X.1qRCC=*TGo.-U<>CEdcHW8baXSmCpDa-`et6g\h#)bOp>b=a2=J2J]B]h2p`IY6T&iu,js??;_L>;%(d/mZmt)<LQFqg%58P_RXG8jTpOr^#n$kO$[ItmFPs(D5,D@LS??8e0ihE3LcpGShF4LZBKANBs+Dd/:s3i[YUVI`SH%=q>CK`E-Xd9;$k[k4#mr8e>TbBYH2J7mh<.@<%#ChZBAegO6uMokJ>hm/2[1's=">ARZb[K,>i^o-E!Sth8CRV7#(4>kY[HI6YA]oG6:sD-;U;2S`6E>`EU+rFfF"oMXFNnMSdUa<EM9=#&:X1-@R.Be7!,!6gnq!!/d+YABU3Q,g`uH6((%,BEJkRI1U)C=g,r`QEV`(E$o$7ip\\gdqMakU&HXk,"cmQ,7B<+:%glmp@"fA\n>K7j6WGh6\C1lTl9PH6!PhGV%,Tt"h#upG+qb.eH$;?G84T_t')<0'*U:a67BSt;nPpDGQMY>LO#1%6_@hk5puao=5T^^L-XMkl7gVOU_KHo&AL,VqFUDI`0M6(fPS=dSNLO3kDq<3LQFe:#W.ImQgqEu"[2cmnilJ_H3u'FE\I[[r;qD`,EqFuD'C0.>XNU+`8mic<bmB+L;+qdNoB8J3`'(oK)IVA[g?*7=B%J>S%bV"R["qVCjf$)\bCbh3U?b;N0j7IerSnU8W+#aDpc#X>7:h6?:,kOb^tbA3JZ`%@'5=]+V?KmM?Q=7`np'<,EkL,*`q!*_3*2n##$RaHiKY:3>Z#W;d[1XjIl7YEk97"Ek_+Xu$@Nto#9'Hs$geVVd,PnhJ;f#Z5.]<^ZOl8S8L1Tn:QpI6-8_G%FDpKUo!)G`LQlHLD\5/6:(,==?9iW7VhB'Jk\3F\?e8pYWO%N%S<T^-0D!oQ>mZZQ8A-FfiGRh]IVLKsW_07WSh"bYT&p(qi1b;*r[]X/=Sose4V!9(k*T5g)SnmC1du90/86O(R`iY.s*Yg+gXFb&c8l9N;j?eA>]AD;T:PtZZMJq<rHmq?>42ek>)WIe4^>JC/M_p4AS,#0]?0l>+WXQ;<R%eU]r/*b.kg%a>D-Z#Q*dsN@VqB[No[,sG#T!k=[kQE:Y3XB6p0mbf=*DQ]Ze$#,9D*Ui3#]eKj+;A^u#_udQ$f?a3qfJ`;96N;=-]A^Ab#Bm4WNG;+X>Hcb*`56`"Ue$OWMA;WNC0DP5p)a3%S6H;]!p\M+H*8MAJGTRLu-[b2hDpkJM6X_'p0Im\14^?f=&[3lqhqg=Zda20G"nTMQj\6F>hcel5d_^78'M^Y6IVNA,CV1d)q+I(%/A<*bRCWnC^$DU;A),\Y,l3rnia@X`eSl\nhBGYS&CbCUQT.\9kYtE)ONQLplEh>Zt')I^B<O4Pe2cT_HZ@IO><f#dtK)nD^%)pKtrXT%k1EQLc\DKFIAhc_iM^^5iES?3b'"Nc+Mb^ntd0jW.W>J5`eqiI&iuG2+XZT,(+IaaP=D9D_6Us:F.b.Q1.CBD0OAua]=I:],nFTfq`up7KN'T;&7IcJ,b2PVT7mhZm+/X8[3>?l[0rH(*Wl[E)c,=!AUFV3G`lb)/#2hQ_[:IP#0WN<ZZ(4/3$\H2e^J"GuW#R@GKB;&I5B"GCAnA.jD-5!JCd+_m#C/VA<-OAn-*=qFs2E5\ZVTJOf$I*uhkQYtR.4]'M5o^WX\toiJ'b4`-k6'`/A4$p4(AoY8l.%<)YE%N2Ji_/4r-os5?VU1KSV#HQY.A(pp[TY0chfZR72IE3Vm]uM0j:QR!O0!4c@CR$;>Xo2aKPQEpC1P>e$ck?".sNQs.I$@p&d!eNUmPcY#-4I'V[EG3mMDq'ZWA8*NF<\Gs;UGUT^891J3WR'MD%jM'GEd6;EGR[PL\m+pP[W271F-sH/(q`al(?Dc#l\0:M1-J9VdO4#._GWnT\qG?c)r`iCGWjS!@ZX6A,TPO"P<S3\HY*W:7]eESgHWX#t6#+0Z''j,RXm?=KIq."D@d3SgAoB[)+$KHCNr~>endstream
endobj
113 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3845
>>
stream
Gau0GgN)%.&q0LUoZPh>1D.Tq8BJ$g.`\fXbiGM'23Y.GV,FN'8XN=""%&<e>PmmDi!$c\'Ld9pY1TEKFT[VUmW/BJ!$/h5jSNBgn9P;V)ZpK^AkOgoc%amV\=T8h3&._1p.08E,M`QJChUC;Z12nfN#sm@Xg'd9f`mD?r:KKuKSfWG125?kYMf%J&4'_l(2F[^R%MB7'DZIdW&Hq&K@ukNqanBt%DS?:>@ZdkDc9Dt+l<hSW&H#mSE+"VA$W1X:r=4WS9-a4n4s"kN@>)NOF*V>E-qBqOoYu0XD5M\3HDEkRK`3E"'\TX6Jmk$J,8j23U@"b(ks.IYZinLI2:jb?jK%(@73*f+YP$\$M?WsSJ#kY!bj]O+f98'_>l9Kfi5[?YJ8205)LTia+[n5"p&!8p_aj;HL_#TY1`BAP7>"\526>`Lb8HbC934qr]LIk&F=uA2it3j+RijM-aCJFX6HL#oN7XAn`qpY9g9#$9";C/7.`F#`C?MSJJ[a+US>\Y/4)Q'fm8Hm*q"o"Re@YekL6WTXt"a"E("QMjXZ:"/-sN=?,R@AIOJ\FKj;Q]FOHOM7[iW]Y"U/o*DC<Zm:k2F]Y0U9%7=_Ie'-:6HnK'J:K6[IrZ+C_gEp/g_;$kQGY8,NL2_3>`$G:hC8`*cZe>JIIO+M6L1YnWl%93&+mF+03r&mgc)-6CRSGL<O,')e8/-KSCuT9TDRFu@ZFo[E2OiR>o^a)h,SIAUK!Q5p,JXVCVhgoH-agV>lp`hC8W[&m3se-344]_oj.dC\eVe#NXB=t:%M93nT>!`>NaXM-F?U](6l*AR!l9`58VFW>qDo_aEa&)IF$C%50:c,M$d#cO:Fmpo]`&'&rqQ8_M;\LEQI,mqg.0!tH1Mr0NTj[:d"f[i!Bk,_XleYg/P@05K.K$WH&i5og.nPeGP;S$I#IGHF1#6:3/c\O9HgCF!N+`:^:q6S?+g<JPY&L*;:89jc83$b(\;4Ba,(8oDIug@$5/M8'NZuLK1osA-I;u;.!CEt+V+)AoS$Cu1'Q;BmNM6glY52QHOO0UMeQ3-\e=;L[5H!m=KMmN1t#BZhTW6p(:-IteI"t2`G1Tq+V#Q;'h7ZkWK*K]WR6k+dMROP4/J'SqUi/E(22:;dG6Njm`:pNL.mVYr0T,"371F;FC8KY(cGLqKJqpnen7)tFht/MG0pqL2Um4g+**mK%oquMp(;JQh`eZ/(,3R!ib1Lh!Z6ALND">L0[oTS)42]80U$KpNV^TE`jRl_O"`]PhYV,?/7*:sU'QoFMee7l$,e($+SIM6/_R/S[cnuf:"MDB3'n=ITA4Fgeh'tTn?6hbohV9SEQrIudo,kHYI*Whp%M4DchT$[rB8>t<o"Y^F-:PmRCu4!A3Go`5qUW'PmLMZ:"-,LTbA)0N1hZgb&b,;@o_teb<bYU.ULR:g^TPJVD!?mM>%.9R`8OC#L)4V;/r'?&hJ(ZGd*`>8j,bh6adInEBl]fTl9,&SX5"GMTP_=Y,8M/Wd:&>S=Z5FV\:6LW-TR3()YC-r!+.5P%)?d0AsV$0KC=$P"Se"E/K=H2Y2tt/Cs;PVB7ZJ,[WcYK.BI-o7bN1A(8P?S#?sUf]"u)#$%hTED0qkUirl"G^d(3L6g@+L0kdqDn!f/\j:#`]pJU6#Y2m:`RWIn>H^+!=\[tU**BNg=;HbS`*aE5"haYQ3NK?p@SN\mQDA%;"X%1;7UtTuT7M!k)Z86nMuf/t2?`It+<)li&]O[pNB=K2e?Q:kRXo+I^fuo9A'"4@V*052`=Hdm(-="\pAuQLU*ge][]p)sY[fqObN\m++Sf*'8ZApS)7K#0*17%+YQC\,cmFW6c%4^/lL-&9V.&;kgK+[GDuW`ABd+Wk\]D7if:%6Z$i^+r6lVVu4Yi]0oChPX$Bq%/$_)a!b=<ZBOs[ltTN?TEQTCGDZku-C;/3qF]E'lYW<&X^##l53#(4Q4:`tu@n0r4A(`=md(t:o-#%MHq\<5&3^Pc.A*Mr'RS(0mRDE2UG?F!j"jR'^iSS6]bXkC;EZq=0L2RF&G]Cla27!7^=WcprL>L(/#CdP>&QCB]XVVmW8/raR@3]3dC3DFo_`1UOBSLUs7iHA=@f";a8bmLqtDR8K-fR9a926=K;TIHF]+AW0#HCj"F;fd'D.*E^s@SHn>f1H#$1<qQa@I-uKYSHJcZ[\20-,lHip%NG&qXOu<-njK?84[RK0^!)i;'X3gg!,Mf'#S'(f-=]L#4hU?aNgaX=6.$uZYe,I&8Z@MXS'7@1p>"9+&e\9Id0Ct2;M)(I/$I/+<lb;GnL='+P;FGVg]Hoe.&@u]F,!3<PA>b=:KZO5e[)a/#E\L71:$*bNZ5-,'2*[6=nM":4cRXV`Q4q)#nP&:E<eL-Pqn;[pS4*;)DDEVat-Pc3ji;g_%NdISSI$!ae/]@D(^h-S]A)q11gB-AiI1&"DSn^n/I^<>Veu"6H=RP98@a/;>qX&d#SYfgTM/Q0'.Pj+Z8)LaU-gqe;`XI6ZL5Rn&2j^O1F>YcmF2Nu\4X@jF4nKS"XtMZn!aDtF))AR#a>U3t6@9n]^U?19>SSIL3QR.<g"J$S:'$AmP<YooNLD\N]0Q'Eb&q32$:G'0_BE.i[cTAHG;?6P(GDWStYeQ*SjqV3nJ#8n-^k%>Lg&`u<Sf:ig/0`VST)DV-l]rj"f%-/_Hb]*n((-E.8l62RR,UChHj;6"0C7;'R"oeVNkoH9UZD$qYN@_Vh@h%TC4;?m/kGTsKGi>OVIE08EPNdr<=c+&>g#;q*4.30)bq<!el!oB\aa9^25_`>eoD!Qt.Sq-i<'eWfA;K=oX.]KU#*tbJ;)PcIQ+pX;R-#Xt7j8Z<LO<"%6GOSoRQ6e'XZYLJFOd)Z?.?&[Xc\cl^/VRUmZ$h-GN45>-,[/ac9&!)f]m#HY'pe=*d?>G7Rti)QZ@$=&DC/2aJcT%cS&+,l)m31_X8?]00"m`E^:*+U1##/&a""7>e^q6SC3HF]t'+HRpY$p'IN'VGLT*/l!,IN7@piK2sQcEj"sg]pW.9A064YH!r:u=?^uqq,J`Z7%?&WJgTWg$rF>=_VXS+e0rc1T''OaCTJqLfmjCZ;"q/S=Cm"bc%eUFZ"H<UeQVoZE5Ys'N+?me]L&&el+5gDCZG6!l878nG9RlR9Ckt@-N/TTX.</-KpE-Fp\X@J!OAn."ar,GC$+o6Nhq.%Ab0:N8mh7iRGl^rjo=@#>rB7l)X#7ISrqIMd-:4_V!QdF.pe0JcS;uQ<T4_(oKFYMR.g"Cn]T]H'j*2#c>=c<T2^KM\"A>^uPm<U;h!DY.:sn\qpU_]V(U1erb84lsL1cGC,3h(h:oCR@(i`LQRX&,ZN*C"8$U])kIb=o'_STSn^o%[.AW3^4WKYfbr':e.d[`5Ua_>SjO>XLf0Z,eI]$n[8%+A#nlL;(l$DpJYQ'/BAEsdYi%7Oaq<2*^^$=i*1A&T$QEHH`q;Pn0)]@3o4=gDT_bj1QE78+$U/4%d%Ys`&pABUZHZ84_Yp<:q*R*<lq)m87Y!/_"(C7Ch2a'QkBOHf40F>AQ?1O7`DdiVeYZZ1$WOhF/S*gu@V(+Gt2UT:-HZAJ.=4gU",$Z^RXU$b]$:qUdoIXk@&0.hI5;e.u4X&8IL2dG._Pb[0%s,B:W:b>`^p$9kFG#KVe=@NNs*<)=SOs_R/bF\J!/Q\CU=*Hoo`fj;^)M_IGe>S!V)=O%aK]K>3&(J!e#pM@cB*re-$P!MGZ`E=V9CGWEpYFel.\C2$L3aO("hb7%)bg0eQ42cLM!>(S9;Gu1OXuWaX]M9TXq)TfcpZ_<[GUNh*I;&O>]9$Z@N5%YepQ7o:jT7SrT.cRq&cds3KF~>endstream
endobj
114 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4585
>>
stream
Gb!S0flGh,ni^@-O.(nonYR!bloL/f>dbtW-FHjVg4CbE@S@@B%j1]AgE=I6&dcX+eWr"28:V/BmgGnAMEb;<\%;Zc5DRX_#+?%n-"s/b_YadHbhr>EUMXYt@mr73$QRF2InWQ6$qD%TX;dH+MD;A9B+0Z-kn@!JB_<^fcc-0VejRnCh1hV85X]&V4+cNXgF8.dS"f6'.7sfiK_Fm%8*5'R7h)sfZ=iDC\6AWU1G%eKA4Y-m:h5*Ok@d`!2u+7nM/)rt;ZW9TmftZ2VhrHGhdHEsk.btfJ)F*+<7;guN0lg62@W5G&dflmnL2.M0;=B>**c&oFN%;J@nq$gSfhH,2tqE6&ADR6MQ%f_aMU4M((p-R&?pi-XTdud3"I+.N\ur)pBP0o.OEL$6^?_p#CaSe8R\SYe-7U$j3TP(UJ!ZR=Y'r5-f"<Nms.HEXC\GsQl//P"p+DUm-Ir]ra5l`8R#k\/,#RE%l=ijjq1hq1CRkPHLtIrg=n(S3P>I>AH=oq4eJ#Z,];U);W[Cc^i6*g@s3i0nOTFBp6/Q!]EifUE?FVU2XWas/A.l+K"(76k2T$!\F%]Hhn+s0[GJddcEsp>\EaL8?U*D;!TN-,5IlIZU"'`pacIYjQ3I_QF/kk,+IA>\+tD:c8ZXS?E9+18(`D-%4W5UEUm5:TKX='B)gIet,k5-LpAtu@!/ocm_1pd@)";_NOsn*+`H)Y(+Aq,M;8Tmm+g&&rdl:bT=RtjO9gA?86$`\X!L(LDmDF(0KNu$![l=76Q\I_!dAQb-*m*V%971Y2pOC'[I_J\!\mnn^["!S;HD=U3<;9Oab%&78+(j2V>(<_6?E;Vhg5Pu<SAp(X!C@$m2LnTB!'\p;n4td327BM"CM;pJB&r,*Pn2slFFB;cOq3lijfi0^3?@m6q61n=UI@#g;XCZbUApo)MW8B<,25=J2&(I#Tr"9%`'%!u;u,le6'%KBqJRp="fi>T2EX7NV:<W_)[Iit^mml`oU/@!ZDJ0\BX<QA!XbdiOa1m%&l>o),k>W-nk*t)COlJuTY4=`/=i!7^#i`0YFSs!9]IgEC^^G?@ioH+];=gR?M*TpNgQq=JNHWR*Y@c4![TRT4'd'gbRZj!pA56-)k^235em3`i,4H\`3RX&1Hi"!VQ.>GaOiKsBQ22P@)UKSJ87i)p\S;(ElQP!mTCN-\&/h_(fEE[j9WiaErV-r/r,_"3`.^IV-+l;+obn3k&>-N$\;bR<YS@kEgc.3S'W@&I1[7C!<'ibS/%]@+niqu$"JG>N]u=:"-%R'Z!eY*%A9#RFe<=eF-XV'L,rjk4mc&g-0d$[3dMr`Z=]6n&%_Q5"+AjD^fE!Dr77GPZ7%`PNH,b)r*kl?([*TcoUKbVA?.=-S2Q=m8_?fm+iH6_iG"!pGlgbl'gbM;<6)#h5oJ1N7oIA"=]_P%O@4PKDfil<.5KJt.6MjhO6=Vc4G@?7Bnl&)`!J$-%riRrW'pFs\fBq=FPLs=/jjO%9k?QGp'-_,="G5j]\p?FY&P(OZ>$Z3h-b]g,u>m+L!J@JZJRl?iPWpnr3^NPduViEL2I`G?"A\=lj58k+?u2_(f%n(d>!gXU*tMNLk=JeX=VobB8&dTIWQ=J9'FmWn5nnc&!2N_,2mNe;?IdkJfe(md@<1oHqH=@XBD(#\YV:M4'gGA=B(TF2^Cs>iWFU(csV!Q6,eBFl=l_7jbt%>A=lj9&';>N=V#UR<@fQ!-LQr;l@@5UXiG3P'sF$bGg.cEciL6Xi);@\:1e>CT8jEB/0h'4(<@4$eZKA3cKUOHC;iOpIP=g/CgGHTM/b3jZ#D(F@G6JakY=-(11L(=\X_u;'lZO13>O>Bd5K1d.#)<af'`uEm!biSV<Ji#hs@PcBZAm?W=Q;C2em(o<m;j3:UL,ldJ+!W[abc[s!hL,/8:l<hIjn2_>L8hoN\U1]1]O_W\j(qg'`1,KVqO6nGYa"D^rHAc)V1KRb;?HosOm=7\U@4XGWoRT6A19-,E:>N;(lpb>;8>!J1WoDV3k@(Yb$JJ5YsbB*gq4Bq&B8o@UX\jC%U?7m")W,2O&MP@k"e3\E(t#uF-@"(+:<GLtYTnI[%@+mh[gTsP,lIuf-Z4f2)8#MA0%46`Y7B6c13#eCX>'ku`;^Rook`lX,lU?dK*[mfIs?_+77]0-!?NA_i4dbUa$9bkc72:,6#0a2MK&%pURC.FMJU$k-d.E75H6>IUdp[Z[D)r_ZfbUs5r7$9_C*7aAnSbSNVburm?T)0cJZ1(*LO%6b;NXH74X4$h09QrgQQ+Y&hZ"o0[B-HJ#34g.!\mneOVa-_6I)O0@Fuc18a,@.L`N6Li.n*dkn&N"%jMGq\U0@n>)r2_NcdUPR>E(B_/UiLeF>tpYod<=`Q/P7WkXV>fh?j,a;p^N1,[TF&j%(6Zeh;L,O!4>#2D:!^j$t0Y3=F4V=XL&NU%,_#ksIKaRM4#.?/efAmMksZjQqePhH^QYno2o2N54gsp9/=Y\u'##j*UtErctOZ(%JK'6W(dODm]%`p>t2Z+sf^I,?h:L^DpkMm0]9<1@*<NN]-kAgS1PWqaoTH>db1UO!?uOl1O]+cl$uD+Ba%.7#*$[?A^@85L1%WY"add<U2Y\HQni@0..g^ak$O_XDE=+Wb'E!B$/mcC<?)p5bm$7:TN'%Zms>HZ`/r45aCQ*B_;sb.SBl'N\.0ofWqXU!'^>dCBf-T^FH>Qn,B?,pU>Veis0?,L4pu/[O2;Q#)9*+eonn`A0Y*DfZ73thef5Ko%(%mV7B>!D0s2.-_\`eOYD.fBY5H="',Ati:P">ElNCk3^R[Fb!!#AhbJI3Z:CPFSh=/6nWVm^=!qs<m8pqn/ESMd%(48kJT`]a(b:W?+0Mg:;GAl3Z@)4Q9mCec2d/fa?QoGI'#\?%-1:kUI1gfuO"NJFP]C:16s'$Z)r#)Te([TC2nSI%Uej(af$iNia4p9!k>MX2Q8"[$$D9nI1sCjM6_D$<-u0/\BRXjqm&ipAp[umG02=Lc:m8U,39aQ":BF6,kXW;7*!\*J7ECQBicjbkmdtZ\LCjj+;+*lfkVKMX*VEt+Ekk-:`I^5VZJYH60o_%$U[=tg3#9ffAGm"t22dWkO&EH;KK[Z^Q4\0#R\JWKVdu^Dm4r2[GD?4e"u4fXU=BB;N#"?C,3D8VP5qFZj@m#Y>0>,q(=F5'Y%Vj4>ShnZU:\6BRVI3,\aRA*"ZKJ2`nLiID/L77S/>iEMg/H0TsO70X,$lb,4TPi+!D"7_oI-\RG/E#@LsUhNg(@qP8H>3osH%tqEEbe)j,A>iuncpL@.V)'>91i+5:F>d10X6aPW[Rf2XDIW!]ge@s`*\`p6KhqE@#eadT%fg_IG0qj[KfJ-`lJa_TS-FUW@5j**SEap]mU+R][VIK)"?^sNP&%7+!^,fG+]3/s4LX,>q:7(VjM[L-kV$t%;o:.<H[k5CRPDk5u03H*eXZ43CW[g@KDN6GmGg3D\]e(gOhE<S0faR)/q`?Lf'`eTL`^Ff;BT"rgD#!71&Y";<B>%-gYGeJWS&p".r_HE.^,2kC_5^nRB;qAS_]R>/BH]+sS2EOOPSkq=D?:,;bM`S!Qiml?#D<.g\T7+ch]nY[1U7_W'-+)eQ>gMT^=m7'i&r+C<BOSAFCR!T"IJBCDM3iY$2Tnje!G#W?E0irQka#[OG*?t."O/[D$'NT\S2!7o,k83a:4A`ccHj&^g&V`S87)^R8/(69T4D.<]83M%rQXepH5d+ciF8o'omVYoc0sfOrqIPE]&!b&\)h%=6kur4c-i".Q=63%5lSoqfY$:A^H_,lf_G]UV0=oL<g?kLX-XWnONX4WH-"df*/6OEJTDh0#e4.(UO*Z"n\anq)0U4A:DLDh0Xr'p[D1%j4`^S.I(u?P$=npBQ^ft"XqtDkX2@+!eTHGgH:G'YFtO-5W-Z"R\Jq>50tocuO"6@Xl4M4b:h0?I@jX._'[6i@b%9!"%1Dp"GHQ-D<-b$#ILhcYEDLh'%B=8e.UU&GSWm?7o)`c1Wi]fD0s59b4lF0FIHE_d"3Yr\iMJrQM`0J16_ELHqnBP8mYb;a1S(7OmHtuRTVF12;#Y[bSJu,5Q<VNK7u%8jb7@$R=G62rkle.+j-uS@[?p_kN:?nG\8GA>EH)FLcb##&^\k&dNt:KhFo(sf>CME%ZLN='6modn[jS_Ee^)E/@.H+b2k/>E3Q:#(kQAMK[k,p`+XI>M95g(b?$1SB46qjE\i\OA+$BZikl$H5;Gq#Y]Wcd8$9Am[91-9li@A2Yl)((/jWS]bm,pBS\@2LpHK/JdB#H+G=CN>69S:8GVY$mHJV,6ed3hnl!hR4?bN>/IoAKHZ8@n:'W0PX5a9r<4%XVnQ`KIpYa'6a^s6*hn+$X*pl8@.&_/M^9"YUJ-e"ho7@LDW2\<K)s7T$:S9boGOY($k4@F'@mlJdcp$C<m#lKQ0epe.'hDNUjmjg.k"nW:0(gHHW1R>]qN5.Qq,AnfMQOg\2rdX#eoBY@JF-B6I&UoHQ1N=(_P<Hu_W7EV\=;L5*qU_d#Va54*[ha.'7D=,,]P&gIcBh7R#DGlITopjC^Gk"UhkG9DJo1u`W+jB~>endstream
endobj
115 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4330
>>
stream
Gb!SpCNJ5g(B*Z.JWF[e;g_M[AnT0QFN8CO857eUJ"];57RN:f8_NXKhocg,3@'-uYhb]fO)7/_%m](B!r#rM#1;DM.0$HZKUf0cm)gH*8QHA?YVhoODTLr)kcZ5+0'UY=h(fu0H3ScdD7H[k/Ul-m;-XejkJVOkbqr+\S658D,nFpS1uHis\P6E1YHRN1c:+U5?ZZ;73?rK[>D`gGp(Yot4,+r%Vg)dFR,@Ol71@)7\tOJ-_7LI8f`Hb.+nu5VER*S:%qP>\=hQ92IYFC0dIT69Ebn``UC7!NL2=UFKJjfOdllM/"2.6gPh[:7!So*m>I(*Yj4t5u249c0\[jlf-44FlL?_)@#C2IB<at=-B%TqrR+j>3jZO$)cQ1:'4HoQS&6nT*VeO/VSAohW#:V0k?!5u=GG]$&hfl8XVApuR8P$<V*53Y)kY'CAit*>3G/)@+&sHV:ieMq=gjCl$Z*[a>/BZO07*1D#Va4F6QLe(TU4`GjcSiZ]cUj#74l,MHO6"dL<mF?#pc=9'1l4amdjM.\8ph)762!Woqcurg:Io]I&6"aGn<`5iml7Wp,"hHjB?23@]$GBtWuOX+C@fe#AWQ"NAK!sI`p<O.oj#l1Mk".WrV,dhO*eGeqCgLgjSZlIV_e`ePhCc2rs4?@V^-pZc]d<Li$>YJr9^h;([U+3VF;U*pA6q7X]%i%52a]W'.5fnV/.*HX_E\B%0`Q6/?C9^]f75CF<i,t,i?Ws*B'1QBM-g6RW%`;(\Hr;M5(kIe*u!=Kq?C7fd3WU;/_hVa1qNjXE/D#LM=Ul+PcpE,H%"[,]p4aK0CL$Y*]\T?lND?;m:#,;]1QNQF!LJ)9d]`@u]/%R&smlhQKt<-$`ncr-3V#pfV0&L[E<Tj;F7hpNm[0K(W/WAV[DG2@t%0B-i=>A%c4\)MbqX`H`dXRaPR`2E`5:,E8FAABSGd./XrI%e;_aIj*K/I0eC*#&s9qWB3Z>)9cj[9AKq]q_eI&!i?9+`J/f+rs+#Q4OWAhB0-<CrVtpNOlhbgdQ,^sQ;g%J7$d<OpJd"YSnR`g_[N)c>ne0d!dq1H%]#O%n@1LsOD6PdX6O4\WlY88WKR@^.`6=CqA5.OM7*kJ@o_kInp-tS2bh'H@$/bKD8)jY'nqNmT'57]c"`Whr$KhPs8%Te2K3-nkVdFsh=\G'[hpOOBYGe6\#^qVY<GRHV*A*bQm2Jo)Z%9J4@p_4Dh,XX@\P,lVWe[pnYa4F6aGQ]1^'QRG$H8h^a_2/\mq4H4#DA'>)l$.X)sV`juK;WF*Dlcou%<?f+YXjMF]jLf8t=<Q9PG[+J>ODn?&W[l/A*5&)Ta$;SSI&P'f=+,@>?WC%uWB4Ffr9]'OW>*Ol%IT_-n`b^RI1#<lq]gT`kJS4)_%^h>Y?"s3YrX^P5&0H]5H5LD;uldn;J]j<67\`bp=a*JW4>eUHHGBpT"./6^(`S;PWGj90P.BCM/Q"YVViOhM[GPQ@j;^T!]LDn5gR'lc0\,I_t;]9IZ'iU6dn3&7($FS.q\ei\Y(NRs_)uO6.*J8Kp_H[)D]8hUFbYic#&(6ObqtrY2a2:XjB:)RKG_Y0cY6%`1QG6]VaMmQ28qf"p8j9MQI.Ab"oCKA[48:j#r4JJ(05_IdG%5Kf1%Z<s,*5[r;Wi=qOXH?s#JRVuqK!J6O"cIriNRY9^==2r\(\;@]IKZ:9ds(nk?7sLM)?eG72>7Lmq'VL/.,:K@`WsJHqu'c<8b-'`sE3fGRQ?R@K<[m+AJ&4!JLrUf[r]fLNGIZ5I*sOmSQ<kZNh"fSKu+-[Iq;^\;MWUfY'PeDZ_D$m(t0AmXk,,_I<k5IMG8uI8JXoqkUSV:RQ0>'Dh/b+ZQ,;%s]Q?)t_pi\.rbtXh>SeMrRYY"l"WHHB9l_f@;.9?E,YrAnObj\I=IFS)chuFgBiO3:'Y/eQ%t3JKclDAE$4XddV;U,#&<^44$d9<^-U4\W[&n-3IN5:'RN(io2"-/oE37=9j/5-'Vk`g\R0PT%u'<nEbTEVTnOo@I!2F'umM$c#nGM:h=Vt:RbQg\>$`gmdL9K*TfK;mbh6hP*g'<4Cqk=-&1M4Le+sLdR'gV[=%7L2K+TB)k(>0U+8LJRc<qeA(6DiME/GOn0]XW0+oSWDi"Q<*nf[Q,<*_#X?lG'LP@qs))O+Hkq8em%1+;rO%SC@Ph=?M(9ojghk:IX>#+VgAdSV'Y_i_oog4kb8s\dbmR3d")(IsI^JZ:RJ.,_M,58-A&n"MLH,MF-i:cJALRV1f4%n-:hl1Z7q"<3`b(+"HXUE50lGe2[pucG'he?3$B'P[D#/;CU%-.6!2V/sr%)0Sfe:^?5er)Bu*aZ@7aKc4K0B5?3>ksfoSoDq3oML5jG4N4(P&48]HASkR'd=X[.N[>A?mm)H<o>8FeP`(#Pc1+1;g(FL_'oTs(,Ot;?N#?J/n/T=(9@V$S]FZB#_oNN^k@GLPCb^b_Lj>B$P-uEaYdWs3ObmP;T2bZ!ST2dO!b@E1p"RS.p/oF5oWj!$-HD)#;r<Y_IEq?g9>>7]=5R?6LS3i.OO%\]r"p:-EEn=*/?(F0OOVMLP#?=K.7FLb*I(BiBcRHj-1#77%:JT/KS4`b`'NLR$i1$=MlRM\cSf!TF.3XZDF'iGYgDuVJI[D'HU4]AOhCaY#q9XS'qlUEcrlX'cp\4<-V4C;GGhB:+AB)S5b\]2PceI9?0Fmc@S0VkmNoA0ZX@,Iu(6mJQ@$BiJqcg_[/V5k7A(P?bT<na^%7jW/+L<(t3-?2NNRg>&(?`@thn2L+R1VXO*WOK!E1ER]_7I_L`\tgfU6hWkbWK\l:g2AK8^7N)#I)(lrD>%db2&7#PB*Mg2m*0BMkEQJU1K(ag72E&e.BKH5.1!#O"]I%#p^2kShd21QP*F&BrdRRAMF#.^OSRGT:DI+'r'[[$ru-ILs[k.DsG)<%S[GB/,4U/796,b"YI#aj^WRdRcd@"?G+9jZb#JBKPNfu/q`=*@#+c-bM<N,S\30s?X\5`!hIX:G2cn82QO8_Qf!Db6Wq]TR?ZL_s7,+sKI^>U)+.[bkmD%6=(k)5l=7dr$n@[P6!ah2M(o`qRgM-6Eop:i3Qe`Gm%>^3bjiC!/H51(I`MP;UV8bs'$SYPpT3rli*YpdYrCRqt&,edd?:FL99N=J_j@]YH@taK%t>99ZP4UtccHfbJE"HD`@#n=:i7,RD=[U/q<8L@]k!HTid&k=DA"h]7MOP-g@&38`<L[cs`*ql_5Ui[EL8/I4NE=s_7&H?7=c4k1+dW(hu3Vebd;N2^;@d+<s_LU350k(?8B`D^+JmI/DKpBB-!dfePPJX]m8@nP^PP@D[hEYeSVQ%*MLP)g7t>MtIkj#!`!hi-$[Z?J*"GYir20g[QoMpUt0RT/@N#AZq*5$4"@a3A!dX%22odP'p]7HJ>>]Y0Gg(B27*I'iCF[bf)\$]cbk=3Y]DBW>IM/G)#-c7[:'Cn%feO%*e*34[#K9W8rkR,OMe:sk^TS$J`8aJNdQ9M"O[/XGQ5aYV*^@9#h!1ctf:V/n+nZltm!U%hmnj/h=+R!]=-5d=gTDlIF7JR6R'KOSLS7VHM\Q]D`)Ff=f":"dm:N6j0D>KA!F^gVXdX9%mD!ug+T-1hZ7A@E*+6X[C,+g;I52IJO#U!GZ4-5N`GM=u[_\Q+fm<:]>#D6E+`'g5/4Y=Z+sQKqNq>tT66U<+LAlAW9Oq6"*Y]&@q'm>g!#I<YDiguWSH6IOq_"Bi1j_Imi4BB:S\.!VG>r!'K=p!Oc_c-#Nlbr,H.Fj[Ef?3$s-[HcUIfucpm$L+!j`Jbkc(o:?f?J,lg8[7jJqN!R)s)B&.mf1h?n2Ye,/D$WMGJ&o"P:SqhKEmhbWUE/FcL[ftGJD)gj>hd(jcKaU`8]apDL:dm?-,eQP'ekaPO]4%_fJ[ioS0cfag5/MZ<6KR?%`//Cjg]?`bs&N+#Nlg]`XmMnWKbFXF>SZd^ob<;r6_bXP+5jq^d(NB-2T:4e7:@ZFd#cBLgpH?!16q,i5d#C\\UD.X3*)<.$R48Tjl9V7V%LY2b<(i)LA$'>7P>0+]?f@P>k]NUnD5GfqJ$?k4k5VpQ!GnF@j<]^s21P"Ot[&4NC?rIK>)7N=Z([^8"s<q7ZlARq9`K!R(>2WcR@'V"PNI!nY%fNCKT.jra!&Q'pQ%or?eMt.N8@X#j]:#CAhq;<mNK/@-r,r*YQ$MgFeOooM8"d?6&964,b3U`_RrC-A,`mI;0iGg0PD397T#6MZ%U&.]2T)8[t`&H1N-XNt5F6^Pc#;2"f\hri+9B/)5i&6DjK=nkEr?laRlp:(E5$4t%<h9@QqCcTIrS@~>endstream
endobj
116 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3571
>>
stream
GatU6=``:h(4Q"]3%n3!DEql&J6as0NajHIQ%^(CbR7FR7VY2nOEA)R'E7qnh-,oJJAC8=5kK6no:=>c5fZ)VrU4V(50T]"@";\&C^GiufeE:1pTpp8YDdS#4j]_H5dt/]Fu3.,LUcd;BC,iBc6rO<Kf^7L@>4fh%@F$J^&MK?kG9*IJZYU:8DRscTqOu0+,0V5b^RCp%nXjgL9k^0l)^"*OG=^Km'*bmTm5:XckekP@,>>a70@'!Y/PshUCHN"1t)PLq>"%4F7f;o6N_I_Udbf9->UIIkio;8Je$"!EsNj:pK&NN#I.m*#hVNdQ,FS#lBpLl4eKL$"r@G36\1m"^'er!"HXa^>U@6>4OZI'PgN2>m^3i.),?A\:\/3Wn7U8AG8Gsb701=-$0d#E*Mq)>Yr*h$/0sECEX.>63?tC!]!,E9Jd+CT>p/"D&)j]Fphf?mpk-ban-2"`n$-_3hJ5W/;;/-ZRXtSNWEYtE6C,oAd-(bd#uOk8U,6XS3SPa76&c:GHi2c6o[B*[&#-1.7,-'=Dms9)#"T`OO2pg#F:u`XK^&+bL^4cL9+*7eE!&s2)%dI5<,N[^Hiiq!.`W'$nA!*>I^2<f^s7^^#cZHFm(&*P8+u$SBK&K4Wg2j^'GM"=k\EGD-j9Oh(VC-Fg".Z@iP@dki6`@uV;UB]iJTiicc(TD?o/T#\g@l5Jo]7na^JZbXY=ITR@Gs\6eEGhhgI,q[d,]0<F`gHK<"03+(c2.?`IO:U,"O,[NXM4>&1''quasA"Z]>f]Cn5oqF_SkKI."tB=!Fm#U^R;10D?83bkPqCYZkKl.ki#Q[VK=N"%SJW)%(J;$7F()3n721>/Z`2[<)qTs'702Mc?Gpi,+;:H<%a38S=701JD(7q0?JKB@f`PEK#@VO3/sp`.":$U:9h!N8!r0gu*[V4iM5k+-JQLVN$@0%hS<j%Ug:YithQ/-9!e@!4K>;7n>bW8n2E\m5-O/&AHLOVrepSRj:'Mhd`^inM(&%5Btj4otem`tHFBI8hcbE$g=?nh0-(-u@BNEEsIj.@ESjV-Uh3X0pSWq1!YtTe]Oc60O<Mo!BNg`%k.4P`#jc!()<:G'g^[!YaBf>2nr19-jt?Q?,UEMo&ko9T?N/h2f$2!'=Ph0l2F`$Hb&Lop3O]l&G;Bf7Mbn\<t5j7V^W%M9[\TjXcEeYb>l%]duJk5B'TG-moKU]+!]M'W<ADJ%4rWc;mb_6fM^23e,aN'Z9Y/U(21?&2U#]4_@*^1Sfh6UWe6\L/EJ#m?k`FG;4KR11nsN;$a<r:4Agh^_1]nT_LGT-@\<pPX\pOR#"aObU3rqVJ:U;.656hjD8N>bk>@>-J$J<AL5e$p2G@-g`"gTD)Bb%`fbLQ%.aegQF%`Omf:NNjfh6r[klHSb8>bHfnI=#I:QL`$T)Yckee!G_gshV%4S1dQOUu+j]F;Jdt&Q@'o^$#&):(^V,JA30EhbWVJ'^+)p4f;,298)oP.b2p"0u/;D8al29\ZW_*WU3l91&RC4ichq.#YdjuW%#QDlmUF32-0au2HH2(aUC*mt1\\JYh$4]U(oJj.<3b5'MP;g+TggGsFL@Z*7;lBn0Gl%@C)6Kd%D6qP;1)kaN(ZpVW&'<Y[i4G4#44i;3BG,%,ms2'8ZI6UE>J=MkOWiLR2*U[BnJ.Y#b8c:c)2Z'*MQ?B@W<6RNX-dusTeSu@QLF"W#M[.*b.RGEdY:hbdm,im#N5!M+:W?fq9>$4F6Ct/XC49JZ`I_ST0oWFJ.>gM$Odp2ISHfIC,e9<(6aEF*fk,O`D8i>7cf]#@"VU5Z9>]Z+@W8/Y^Z7h4&?U0tgOJ4t?WYt&*81Jsb+F`eXt\Z"Z;A"j6l^R]@&aG'ipZ`ETCp+7_Mn>.0MWAfn0o<l=BHEUVec?c?=<i+Ooa#O-R+m)jU^TI5F[gtXV@$;DZ'^ee-s%AZ;4tg08NP.Wf&$_l_,=t65%^SGra:HbHi7p6Eg$06<&;Z#c-&e"STtoUm4ojGXkg\*Je`MG%/iB>b"fZf`p`k))h6!XjJhpE$c\mjI?=uH,jKLY;)aS;=rWgU"<LH)RLs_XW_/O3XRQ&SMl86JLeL1`^95okn600&OI\YZ8$/_O&em25)J;`;U6QEEsWXY0oLKhB]uA\p&?Q8rU3h!WAFoBe^D%$XQZ#5[K;e]A(I]Z]mq.QqEp[JJF'Q1e)GLT0h&@e(s0B4]C3%/o\F0&\p8*;>Hr'5pqS:k[Otl.32j]'7JPADP_Qj!6p#k#D)Br<-!)$-'*qIi$S&OL"#LgY&.Q8@GUr*a*kDOl6u41FWZA4=jHX"4j]d&EG?f<N0r5%q=Crl`7=E8Hd7jt?\p;o7.lh&EXAXMt4C@HeBn$9(5,QH`9fHXt%?mZ<\\b?_%(A!!U[;+95`/3fErmA3Z-Si%\Y?-,OnVNj`o]09lf6/';f1Db4Ic.JIFh=S_:.iM"U-Vun#<`jlG=?R+`E_\J0!(q,,6HEJi6mhF$+nu&6&O?`3[-*XMfF=dMFIg'p37^]-E60I9:BEcqq.PATN2;#8-M&*YoF[9Dd:R.k`eNUUN_0mu[Yu(e5,;KcY\/QnPY3DN`b+]BQ^&k>^Gi+euI'I7-Zs\rHTTUOb>3XQH[:3O&&\B7beTK_a]h#N.53'S:r!8=R6:qa)lQX%i5pN.QOR(e#t1&:nh765XDb"Y;/;."^C<"sUU.kk3jk\4VZIUb7_k#GS6Io3rBD8\RCa??UIg^1Q>%eP5=q,SkQ]RmJ#LZZ,_6p\Y>#lQW9eI7=h@V.o$?$a[mPU+]X!8=EMgEbm)b<->lE!]`F4(t-cBqcFWYM]c2F@5e!d;GF9&L77-2R_c]m*bn/*iTrOL`sSE02[NdPa(XB#QEBB"/(SIN9O_^]`W%$]W7RA(^N)T$XU";dp`#)5W_u3WAOk'GWUpC#Pu=I=2U5upD%2\X1L`9S%V?C"FN3&<fW&_,G>5r>ZMD[+d]eM\W/ci1!I[Ro/D^(Il9J08"6jE9ct/4RJfrdD5137YTURL!$"d;&D4nZRg?&a+,*Vc]Xg3Ic%I5n'?=`0o<RH2p8=i^+j%kq\N0RL"X(u\`Dj^>uW0V[]I(A7'O.d8?!g%pd,XSrRLEK7%$\UV;0qB=MDB#ht:8:od^0[P0ntYb69EO@ZU&L=DlNG0J/f3&r*,a]X^6!([n800*UB`PTU'kI!QE<X@3"e>mn9bH*3c,hq,DR^;Ha9B"`W9KH`Dl6dEHp>+GX>rgWEP7Jc>dV&AFf"AHhjY3*U/3c\fOJfo=4ug;AN,gA?Z1FmsHB46]!RIFVBa8DJE]G]?YR[^G@Sc.&A$M&Y=YD'?[h$AqlFM95GQ?6h\lg2lJ?Sc$Qr//&0UID\qte15TQgIT6QR*(o'FCI>e7fjdNr)Pa[7+"!KqV=-EV'eD[^Co6dr!(rXd0eG"_MMIe5E'`):G2[K4f&LE_[][5(PBUi&4;dX$$Fr-GhsB`#%tWICAc"E0OR%qJ?_D.OG@N\@os&pr=<^%_2t`A3H%kZ<5.thRk<IHTN_AsorFa$P'?/&?r#oEIh]pF%/_a%],)>cFF?!tX_pptWFRAS~>endstream
endobj
117 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3635
>>
stream
Gau`VHZRn3'*$j]U+;SBA`Fo/d1]lm=2["RDj-PrP18(YasEk".+'PkJ>qB'maj7@+G9`'Dq3O$M);!qSpp[MAI1d&QM>CA"*cVQhG=91^gmY4+t,-j&!rKD+"'t#;_nPRQ;l4"*WOSin1hZ"^8(Q=+#ij!-u**3'%*@F#XfA=02@27-L-<ep!<OZ?*$.6an\:CFB/8OFChlETaeTSXb!n$%ua_5S`j<)A4n($kTj(noa.TsX<bX#>>s`A!hJqu:SPXYco$4YJa]c"[POgnPYA'E^aWO$q&(O:@.F@[VdS5Mj+H[9amIEnD3WumI]qjp,[E+N1gilO`$#T<W4.Fn#1u"O':B,pW6(*n/Ml,G*nmoo$]9b#F?FDmd7/cU+V&8e=K7k!3JI&]:'(r<N_<e7\iOYXj&p-`+T42&31b;?!ut+KcOHQ621VbM*?D'IeVWMEe;S2LdUX8l^(![!qp`S=M<I8SK0t7;Z+d[R$UoE4H]^3u3M-#^Qn-F%4Dfr0S;:RJ\kZ[Y$(ik?HZUM8-M6>X1o:X!`>!3-hXtc@P7^L53,o#7DBmT`7RT91W&hDMeAatu'3?#j]oA#3V_(JFo"?(EH"#AQ`bCYO$a,R/gV7t1#N0->6S7'@M3Eaa?@F\[Y(PDEL68o$>o2kCMHa3HfQ3YTF1)FM8'l-bRq'RsFr]C[;k)KkAJ`$!P/uG;_#ME(hbF<,k_^iUrH%o16r7=6;2,>tE,=UP%GiKF#J4>LPfr(KIBgGE4.>p0)\;/u-P/19(@\>+5\O,"^oLoF958#=YHe<<!?lXg`i6U!73JCufo[kQ(Zhs(.%*6NEd<d#J#XA^;$G3@70V#2#eDYY)Uc,]lRN%Y2uGpXMc*nL"4TO#As^mrh!dprlU5muD*Nj:e-fCpMpDs$;0[.bUpY[RIGeuT_oO]:E0CE\'Z7_/X;,3aKgV3D:2d5T.g_*Gk<,\dV5Tdk=dY:%oEi:8`r:*OIJAq&/o6J<fV?hW[J/b<2,c>%It+.=KuOB8)]HUD+FnRbJr-S1:Y*Ge95Vd>Wc>S7F<*Q4&!!!&?j4907M5;,(_7adEQh2udKn+jY8394H2hjp8s7jkLEn:nbdicj(loPE4SSo7(oV@AX"_@@o"g8)oAU3Qh-er+4@sF4J[JI20Vrl,0AtiLl&QPM3WuAo'1ITc_Z;g:jEcD3!2akXUotL=mQS06b;-njGM:+hOsd9:&lZ4P>Ab"MX:[hYZ%0n-on%jKBiui-hGDi3]oP-0N+oY</D<Ea[86Mni#tT</KA%RUpVfrA\V)'`N0-)o+C,O-ohC27?1q7?`I7gi)e0,bkL,-\SYbDOm?5<:,:]n$F+po7MOg5ICpZ#](ADJAOs,M6OD'n9+dT1F,I`#-e'EdSDl/BMRsSYi"6$+ia7iVCs#uo*#l5IPH<]j-E8Qg+\\fkkmFYo?d>3qZ6gV*a(se<Wqb(+J1^r,j#IFILC85NRWYODP30c$jHKI:Wu?#J?m>Os<fM0dSg0;!Jculi$l2dNSJY9KpO@k?,mk-J5$!e.6]U:U[Pt"A.O=&2+hr3tgJ6![_]Z8KH/NtFcOaeW,N!UTnLOu^QGWfAa36kb-,1ct@b/LTDGUb<gi+Mka\9sb\.2m'1'nhorksNUr$2(U?meioO#EhCf6.r%Yh4iQ:hCm$e%q[$0L$/+eeR_1A25sJ&Q&t4iFNPITTpB=#%e=6=q3'M&>p]CWYPSriTdft%tm[5U!]4e`:T610\b:EVf&_,LRCS^#?N"&j+6,3i7f8Q`Um1he;=fhBFn=36'1p/'EfGO:+>A$\oMaHX%?#i%ZG!7j5=Sd=,s354O5VDnp)k@1XF%Cj`&72WZC-Eb*rV'i=>U,b$Rh$XTq2cl2?f[<BQT)n?U"A;ET+RnJ7$l&AYn2+'o^s\<c1!0T:^Rq!]C/d=$F8J]RN_:%8@#5Vr4"b`&.RA'Kq:gdQ>R`9T\EC/E0E(9/Otc31[N2Zl!\r%5")`S(q.9_OSt6tV5dUId2RqI>m@FFiOo@at+c"B7*,k!>LOa&kRG:^NkG6[\N^E/u[FL@W?+KrE_&.1>gCS`p5KiqW9;DDKm(Yq-XH8h-sc-]EY4eQlk.kTi_4T(RES\j"lLNAa/#NT4:\Xk:,4$S*TAF3$^Y5csHQ73J.$]7\mO%bP<%&dHV+H'VBWlmN8M8p++0*1D.aUU+uJoB<8B#-1U4!O%@aX>lIK]IF]dWj7.>#=`.HZ`[PdS\m#3+G(bLoDI_[3Y>o5)#-$UJitf.9LIF=[Z&b$&usbp11r2UJkrt\7c;Jj]*2tfqY)ga!G>qr@-[.^iW%D^dhqmpMukgk+rdHU,\?3*o"h\r*-t&Q9B3A$bG!\9`Q,e?K=q[mhKJesfIa3#Hih`2qB\d4PJZFm`^sIr2fjY"L]cZ`NR$h3>t\=4]55GZo+YH_N+fB.mIS;Q+/Q^E[Y%/)SShlIC][YG/dV0;a:>V/GVlV6f\0#uAMWh9D["_6Yf7]RR-3Daj?.3N2T=VCFVk+e9]$B-?d'p%0m"M+ep!S$!]NW/VB'&QB"NJKfX9s[-oahX0";6dfI:q0<CZs=)j]P7?j-]144>\cfI$7ms!?=CCe!+5Y^AAeFTXPT(Z775p]=7_K(EQl\0%scMB0ZHGTTVG2dZlGb=e5"#n%JI'`(ttgBc!-a2]sh9&*thI;]j-U+#B3EP$ON^Yr85$i)lC/>e0'e7KbRblSTN`ngo2$H.R+@rYU1dVjej#?+VdhdVO[5(\bN&Y%UN/6oS(^KS6-QWQjq6!n"k`X?(`0b,_*n7RS<`OrMtLSk"Z6TOJRB+:8q-M':!Qb\IN0Y]Y4punD+,QW.%@a1=i7sNC/rS(NI$3p`a+%:!FJ^5^n*gr/$_atQAn.MgX=fO?"e9,smH<m]d/2FrR]c17<-a2,?_n1:"2$0Xlo5oqhV\mcm!c2BaU`@$-cAWbQShU[:K@CCIB&,1(c\]O+D93$uArf"s;oJ:b\NkY94o[\&qId?YS*eF.nQ7h(P-Apn<)T]90'VmDEgU[A\4OMW?dN@.mjr=WlB?V5dGll-bab\raBKKe18L)ps2FQ*f\Od6h#WJSQfIDXPH0a_U$D\73-7Njphk;JdC\;ASC?jP6.]^%X]SoK8DiS3C]4DQ?B;i2`YTp[HYrI.cH'g@IXX;G*4!/O)a[odK?tlD0/qD84S\t^(ssa$d(b8K@M@-*a#;\Y^E-+N?BKgBp82OS3Gshq$S?K=iVN7<AC2Aim,1cSi:p'a$>M/Po%-MciU=nXe@qhr4MtLCZ[d80>9lN!"k`YF&h??s/OG3j'nCJc:M=o?knb6uZPDuo)Q'sqZ"][U`QV2`C`B4_K%.9&RmM<")tOZoLs2F(/7W5b[Jk&N-CUUh)u`0G_&N3OEFe9B>olcbRkqDg7k77e6TmnmR^tkAB)%qKN?npYc\08r'=d/]\obU#7Mn7alO?+.l.L1+o@YDfO$A.uo)PAaQgkA_K8L`1fg_"/0JEFB_mXrc\%EN&m)#f)5@%S+Ik#aDkGlnSJ'30@/7CAqiAFQBVQh5B8!J&m4]gX!-+MQrT$S?D=A0A(.p_,^/'N`!!KO@t$mq<b^L$p59=YXg3938\G.\9-\af7)MlK)K8OGaKCp8YXrrKE)VI]~>endstream
endobj
118 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4059
>>
stream
GauHNH#oHG(B<9ai`2ge`gJQ;k*D4%=iYM%n)R;UA\>!?Pm]rP,"=hjAQ^mUn%Pup6O86rS>l01kiVP"If6F(J:cZHr,q0&Y=?lc6\)*hRK1`H*CfMU@W_80Lu^@76T/dKdD>)]=jd(,5'QYJ+JFuIR6-@)Dr;ndOWpYMdj(YZb!h,&^hAa$QsfKG'<q96a,);u@aAHs*7'9!<;O&mh0XE-<PY@CB+ZftGR"-M(9&BNiF>R!iV@O3I>-(PSA.jZ*87i;=iB;>2G^Pn=j8=AgZ2>Y3$eq3rsh%i3NEfAjFZCm9ji2M.8<`*$3`(fF^ioVN/n3^#7r10d+f-TFV?uO>e1V_4JQtd3!$r3BoH'B!rD-)j+*/^T5FqbbdFF9>^'1q^I@\9.13k@.p=Xuo(Bt/=6CC/(PB15mk9E.mqrB&?i>9?j`bZq+BhZ=o&GsM`AICOg=+Q'i<*a>F<3Tl^#h*c=[@_KRnr@:'5HYSg8=O6h0Q9hKC;]WUG/Jr9-LcODWTnl_="$dkeZH6K=Yc"<6R8:'"=Aj:?@?'paAYSo[G"lWbg_SonTjc"AXaYLdH?/0n(`r*%)4`P^JJg0SS#E;n0Ak&Cia(:+:5H$!iR`F!rUOCN+_#[SU4_h$9BbYrSSp(+<K3BMnP8i0#,=V3s(nqc^Vc<W&(WfXGY)o_o96\.$.ca1]g7s5jAC]VH^/,2</0:As[eY2YZle<t5jF`]ft3/J\X-P/`U\<=nY3s\E:;TOK%i-UJ2$2%%:?JG^u$_3+nJok;!F,OY68X2:+#'RH:6aM-ho3M!J?@<+p^M6Oe;"?\OG,Go3GcVhGk2^Og_734N7TbBU&_tig_atsgI;rjI7MGE5aBpD>a&OjPi\!JpoV^a5%-%J.7"e=T"_qehka$gIPdrq3,Ps"OQC?;k4)*@FWUJ6G0l!LsN5b;\pFQJhnDL1TQFs7'\$QneT'Zs]]C`uX.U`qP\*rNtRa/S"0`OaZJmO)qfYXa5'@f9HeJB1R^2I9<%r>/Or046'&cVnN^7K?%`NdQ?K9IFK3Oj?Bl*D7"E"1D@Xrr]+T5@F@48UpZd^0SH7E_4VOl%pD:=4N`.;>8&h_ttnBHC1)'k4Ut5a)biT2.E`\Q=t)Y/3shi1^7c(&.PG+@>#RBJd6]:5!OA&Lspf-U:Oe2!G'&Kp5JP0TF32I]g`0Z8aB-?'*gT_%d9J0gS-=It@sYc_/ZeR^MuZ%&R`X)^gm9ZG@W(32.?(kfX_=+CZnWXl)>20ds`3R/*D)R7@S0(O2DBfI;n#[SftIo<),P9s)uonrO'<j-BVGK7+2ENS5K!a"s>;1&1S-gqQH1Ao4'7I@J9]'U`VrZ8TVabGJ<ND$R*&0:M["<>TKhUg3"O[=Nc3Uf_"nMVspL^<>A&.Jo=3c=bY?a,8qbNpG,\8KhA+i^ampftSJYq_$.XI9A@#d]PUTQL0(:?Hq#QHrK#>`=,BU/4:%],,gQ`@oQ58@$&E7EH?CL2DGHMPP?TpQRE8C?1KL5[7aYoiZVCN!qA/\^9;lIGTm94UIF+WgeSRD*OBGlB50&cB.7*F)M'NV_Q9SqX>@)^(1^iJkhH?/3T4#V'FLWl(^r'P+$Ygh5$W^#"(a9B@un/T8<OcZ$-P["G[-)Cdau]jP/JQd6gX>j$nYZkmc>,JA^B;Za5P:rn9)9t=+.$Q-=>j#r;:"Xg5BH0Q'K(c\)LN!8NGbV\ns:Ad(#$tHcuaLbh()(/^TXO>WJq`qW5q0h.hfkmWoHp#g5)a"?Z_$M[YD-1r]LD?Ckl,Eh/%1ent"=%6\-PdlRH)(tB&WU7`+1YV[Cf]L229d1%%kNrK,enVS,sB0km>]Lr8LWVd5".&MYHQ&rhYPB`/n&_kVOm[oZQc5hkP4ljQUU2(I^1dnK=4`0nW"M]48WB,_\[Cd(o``)"Fjf"Pt=G#F!&V1VjYK&HDM*I9=*r:*[).(&Tjl5<i?V7?e*TkZ*1K*A/i7Er>oSduk7W/,NdhhIaX`CW];_F3oV6_)u[R8,))e8moV"/&5E52i92B%"s,5DWg#Yb/H9&+og!n)&P3J*L5gCLQ@_]U(u',HpqTgZ"t9'D#(gAHJ5U8QM*p@atfBX41)GZ+Djn%E94os0TQft#4u#rsiO95?T'UE13&i/0W-P!t`>rO22;S,f`S<H9",.Ra&%dYn\Z%5HhLA-K`i.BsUf4^DFd99-$=\`gN:p\S!cOXDZ+mA"/_Bt!b]1ej/K;gVE6`k=g,@H7kMFE$XVI;!LE%S"5`d")GZ.85Dli5):Kg/XnUm>&*0W!@#:Uf"Ork4^>=CIY@Cm*I*DbC*<'0.JiU0C/Fe-[KUdUXIsjim!D1/E-t00<md<X\(Hf=j`$+W^\K(bDNF]rWH@kVC``tb>A'si>dRH(uEXpO&q0QJqQE/9;BV48<SJc;bWs#MCcj5;#d.PA>jk>@SVD4?(ANf0&GDA7@]=8=hs1?,IM"n%8:Q8jbeI1#kWhuXr34'bsDh;<\1*I"M-O@68'./cYF`MYsrhE4b<A^GH6umFABPBK-Ra/<Q=I6Ug[7S:oH=L,094SRF.7&$F9USXgC!]alh9@#&]`kXkWV420en")pMi-Wrlnoc&&5[WU-lHncSaV]!,>:F6_&@cqij%Q$PFgE5Hp4WDQ]#412%H<fFYtfqga:.c'a&jt03@*Ta@,=99Kk1e'[TU%>2b9FKj^_O>UUQW'_5<3Ip$FptieeVC?RPJ?tb^K$:MCQ,X]_$sSfk]<D_oA_nC4e9do0$<Uh#-"cqZ#U&9T/G]M4]d:7lMZ;,3&7i5b-RV\[#K^.b'hYCpSg+f5nK@%C'"%%)D@.$@u&;i@/EdbD_A@pj"#+acAPJIW!PDL*KY[4>1[[^K:]#[KrR&[ccTs9_p\S$hlfPUX]i;%T`0FcH%ZLWZ:L,?l\knK!k9!J9(fp(EEcSlX3>6(KDAMY!3_7\bb6KgGZ>/[k#<56X'u(4.KfHE$t'uQ)8N:hJ7.=_XNYr?01t(X`QSb.LWbU'petBlYGu9]^=qF'<`N)L%(e[A<Y`$4;"JIG?;K9D2;-;jJ@0IqUh1@b\J)iCMCqUS9(7Y/Y5lHo=a+geNKkYJVg&%D_Il?C3kh@!bPF^k@9ToRYLmQNdOsoc@b\1o8oe>,2-NK5(aftjqQ[%hTemnuIaI[Y3>/G?hRj,=IfoqF\$qTZqS6&i9<-OA52a,l]a6Ws<X9W.QHA0bog?==6s-WfiC.%M$:/NTW-eA:aMFkJ4$%5&Ur8F5eGE2.o%"Da2U>O6P#jM@2L,K)"S-qqn>pV%s(M/O\g?Z7?[iF]%hW%?Y7@]eLiRHS0pkBQh%sicT"s"6_/d]4dQjpI=:KDgpfJM/mdM-k/c/"98"K"hT"iYEA@%D)(*redg5Q`:@+BP^LI1psrjT1%kW2;pdmTY_hp;a35c4o_lW.i;LY[32Y3b0D/<7n#4`cc8A`YtBDeGlD^"u4gX=)239pYHF!pK0<'LM5i@HMJK(9F'TWJ2u$$s)c0;Zh(g)DgDiQXpD1;0:sDQu`=HihSB;CFRpnET1-4"$n5DXi?]IFs2$5.iDa,i76O%h\oJcA]leb5Y>sJ<.>^[L:loCn09!cXQ82kmFSP8C^QZjVKgXg[Y6F-f2_EWMfSOf7"*ot&@qN]WjkFW>OKf24IS4@^Fms:4hD!qEG>6u3WQ61mm=$tYVh/J=R%XHikRk[3Q;cI$c]Q+0d\q9]nI8@6ObU\7jrKr\F!RH$Pr3AQLAKmd:!_]p5[^#!CX?EhVY8*7&_VAg3`pZ0VO&BmgmSU^8FTP50TacmBS#u\5uV<-!!NAN(Na9CRRRpO1AJlCi4H^1\]ras)5dr*+t&PP*bjT/q'`9,!2tH]](Rg2`o^]FkGC'CK6t/<sq1!A$rn>fNbR[$1W,P3^hO6.q[b*&\KRtoSim"4eQ]B\8"n$&u'$K'SU7@Z0$YiQi]$j]Y%@-CV$*?=B)HB75<Y&\f+[Wp(bKtn]$3_'GCs?l+EFdTMe[bbZP[!!RaK1^[*]2<LN"4SU&5do>TXB;`rq(6$JD)mkj+o,!M+Uj&%f4++9FUkl~>endstream
endobj
119 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4599
>>
stream
Gau0G>BAOW(4OT5^s()IPh)RR&P:+/bt4GDSMX2RFn=U\$q*DpCPlLcb-JUbrU"qAGS(+B,3;LnfJ3Wg!''`E!A]H@015a42MA`5PL1>%(abY(M)!T$AH&0*c_(13bfG-kD9\L$qO"e\7+8:&(reit$oCR[L.15D@d'8?_i`j[l07ab<b,TOj>iY.!DR1W1q3krU]-k[/.VKIq0@$FE^9%ddj<BkL`WrM)q$]@[&*csW,])jOsl,rQ4b,3jQ1MMfcdL<)?rdU+u7PGP"S)B)]$7L_+;.L7'N&BKPuRb#[``oCt&%bneVt06u5BM>`XF3RYNF;9.hL#q6snmnL,,7,]Q@"VA10TdscpN'B:'6Z^VhF,0*H!cE)$RQ>RO2R(!er\gN!tLoD>i$1S-memo0$V7u$>9$hgRA8HrCKZ=C:@)(Xt^<[j:[nZYRHa%73pqY"'7;Nj;lIo032Z+_S<b0IR8/I5^pfUnHQfG?I%M#4tM26oF52.Im3F:`hP;EP4P/N=9-fj*25.a7`Dm!H3@mJBq#njNKI3@C8#@9(:Nh*)kG$O(NN/JB,G;<0B`Zo!p/5I/Q$]VU:Lr2\NUBUHu*>.f[cNW&M$rn9)7I\+eBTeOj_9I`oT<>P\&#@4JH2"r^5:i_ASsUulDO/@l.?Q"fGUCKP"cGP-6H4h)>qkTUE-^<Gs#h#(Gd3rVb5<+loNAH9j8BCWCdY]@q<&BgI\4m53/=?@Y2S@[boKJN1\2)hldC.ibQ7[8Wk8u\_]D]F2?ERQA?-pR(Bd\=FPtJ^+IFh4!2SJ(B=cd)>W!SP,^m;\CoGE$;piP:FqFqM\qL>]Qc]XNSXK')+Gd[LlQ<YHcrY6d)#'NcS:[#'.+U;d1Mu<BGhI2cZXP'e0a6.urp&)QGS?_3?:Pmo;3g2"?>-]T(H#'lq)7k:K8&;A%gL]V&<!?B.YH-;4tnZpklF&*1uXQ$ng!BWMHF!P(Ro8>,EBdIFRtnBGb?n'QtTUZFprg,K#usFf/$kT'2p9__"k#K!E"hMp]h9F(:38e1S!]/)dD]QiOF'YY3=Oa2iC8$B%+s6%uU(MQ-&+YHB6VXIp4_F+89jh10B#p/b5SQXf>Ql9%1uL]B;Fl9L?IWK),<[."Q`0EHWA(f(.;EgNW.lIJ<Z;*ZVDDQ5t-\<ca\L4D4k"mVtd62P:.V!@\D2]lX]1EN)PH\aYJGA1*rINX?ji-BM2_#7,[<..Ial$U0qfLr;Q('ug99",jsVRU??*"*FhM1sb3&6AHfm'+gIgf%Ym8d[uV4Xdj>a[5;]/K9%Q$U$36>Z7VeR;b)$Z7!!3d3]>=s7lUk:0t+!KBp]AJ!i'/pbAVqW^5>+ch:+rY"NNoYcUU^S:;ZG+dKS.6(L>d4)K\C!ZCaAuQ?[/X1<2>[:l1%CP2KQ[8OkX%_hN^*l;QVK/rf!9BP%i1V`p@MnqfL`!BpZ<TN0p951PTH"HOqWrsWo4mC&a*3V=j*[$:7n(\pZ&3PfF*guDi/L.[3nU2<kDKAZ%Xe+mOj2n+ts'B<=5>X(P`Kb7@sKKcIck,=K8(>d7up1CVg;mX(ePa!=>qiI&&]<]AM?s`Y6V$R(MeI4r(bfQ\qFu-mS,Gr&7&!\!'0=Z"?+nq"<ZFod"m20?_q@_7&3?_@7Eir5)Q<>t0dXB?N79[p^?_gc;h^hUphuB[f?3!(T?\&0>AZK?D[rC\J;qs3)8`pP-O<(3ZY<Q::eO=je#">eNFR')Z1MD`*AUqtBM4ooR4B_q>W&Q&sE'c`uk?Rh1KJIsI2aJ9W/GHa[<(t\H5S?j`;'7==&<Q^aM(5b=Lr"L_bJQC*cniKdpA&]bLlEK-QV8Me"8R>7@Z$J1U1JX:^m/2eHcY8Nl#d1LmXocPStBE"H+'2lbnf%Klugk'BX=t`XYAICG&B[t0jG8:"=/pL?X9UXpU28F]%9&3jJa'Q/"f/9eR:B/%4kG0kZVR8XM,8t#G_eri;eRX_De;aJMKcP!:k`q*d9+9L!NNMKIlak$27]Xr^p*"1/];n%H;H4h3/n5Hu@<EIKWC=PMFj3L`qSQ>8V#!o7?97HDN<++\oA076Gf;ckpI`3C_`0a'[cfO([1bFN:gUeFuaWT0]`e.oYfZ+h,D1OPIu*^"T,$IB6L2<k>XL_A'];"HPtbjPc@>GUaUSqlVck;pbR_G,552M?`B[''h4VUs%<AXh;KSX\8h9h;uO_h3RJ>P/I"aTkOJ.XjFC+"7hJOeQV:mVQMq)LK8gOE$nQb/8g@I0Z'in]Bln5UeslfR0/W7DT(ebE,/R2%B^Yurk%>NA-Znqf)d,Hk^ZO<A9\81&a,!;LB#CW?C%29R=U%^`Hn/Z2i)]O!!`:e$.d#"!0K:*G-^TgGqkma>C"V!KK$0Q"BKo!gVSfn>hjrZ>\qk1n8)>2((UGj1(NPT?BBst^BX3JjYXrH"2rBYJ!SU"NScNl$<o_Q"l1V;0?A#OG\]]CfOHBZA368?A<2h3+Mp%3+q83V4Fr>Vl0Yh;)fSC>1dsS[Qq%a'H6O&i-Y6D1+B0NSI"XK14RO_/B@8?j*e<@s"sGXQU8'<"4B@[2HbQP3q1[VR^-60eWAVa;%2lu.fPV8B"!<7OgOdT+H8sZ@g^R07^ID9GBCoWCl:c`i4D#I?%.]*oMhT2%P_*5i#FH[NS7@dpM55ZrGO'YX9aS6:,3I';58YXU]R.##`5m=l_uV.8YgEVAkpdWs+0=&XNoBI'TubSn&,cuQ`jT)kXCiSJ]\4\]hiMCL2L%*k+Z)l;#.Gns<``;bH&uVC.qgkejkO%gO`d,4Q_P-0PG_d=n^^ur&KYE\m+mU72.M*((\(i"oE%+KDs50AYr8l,2,red_)0=FN@CZ;(9GI;.PF#[7c^N79:Fd.aDZ)jK5\')X6p-pdn/<l4YYi72Z#Qi=[nT<C1;]W%3s"kj;$Qg&BTW:dCYb00&jfG(*`8&@qcT\Z`K0ZYDD,:TDQR9X#4>/-(Y4q%4K^pJfL=?UBZZ`^W^dL.Dj7W9GOmt_h'p7eK\>*)>\HF-U<pVE@:BmRO>^hU6t+r+*0`]qc<\j_$7oVD*AD/iUMcq@cF&d4G!"FdtCF3[,5tr-W%O7A&:,3VLkf_%_K:V5K6V=]a'T/6"Cjh,TWbu7R`cGUeX:d[f$q[1`5cMRsB@#>`/H4B5<,sN+RUD)8)2&1AS(HQSQL[;dm8(<NFdrL.=h@Y*J;;16]-G-,[!np`PpbNs%V=51SU'ee7W1pMA_S*M:-m0Nc,ZbgEUQ+0LD9#2kqcM]R=.?<<GS*$7SVX0LI`J=HabrrtCkOi&mZPT$PmK4l^t$,",9?.5?*k9,/A0).>>hc-SbV]i65n-t9WbR5!W/9'!hAjQil"A.=HRZ2:=C";"TCb+;u&<@Kf/f!3EFL=cP8q.r1:#M/$_/d0\nZ*FphiXnS`l5<?(&oYADR1S<DJca!1XhQABV6uHW/E.o3gJ)Pqt*BQJU%s<UX9<h4BiaL=j[XgQ(+Bt+*8+cjad.SZEQY7HgU98?;.G'jMsB=dcC&g8i8YZQB1e/+OSO(jc+Z%Mba-I;EN0jfAAW\e!7N7<!9E?Bfh1OQ2R01^BtiL-7]Pp<&?kW=D<;`U;8qn"jI)_S/Sr\iCr?#3Kl2&UgckS0E8Y#1gN0V.(5EPA[CL.R1ppt=$5QgX\bXdO#7q-cINoVS6DAPG<!32p_Wjl)9j?YTk$Tu$)1f0Gnq2[_n0/b$.E1gR(sELmqPh'!&X(\OBi\`<KS7J"7[+iot>6U\BWXmb$u1Wbr9nT2GD2367'<8EMQ!W=6/M]m.WLX`[(4d1*$,rmLXY^C%qqDHM-QL2F#naiuT2bSPIFQq2QK?bWD?>;8bO$fr^`m*/0!WR-^k69&k7bI@jJ[7sX0=mfrp)2I`XEJsK7kD*6VP&`0b%8q\3M(eI=gg<!2jcRRL(NZ-A\g#609&*k1mBGS3!:g%0C>7"!/b%ji:_j$F5W\K8cl3S0?PgqQT*_":O_644j[S^)1q-`IN$F23C_hsm=AJC/5\f1'@1GT^S^<bXOljF^tp@)ScG'W:5dIV\t'p_!kG6hK.Z3HLtAc`!-AZ`qjh-mT_j#Z6k1<>M%JWGY>c]VZNjVP@P>rOSe&ACdh3n#]G8k>2)*[YSpoQ$P[-J;jj?q+]%$RrdjE@Gg.!di"-L@ZNk6AOI]7G4$Cs7,8MFBqp[5W3+5eth-e+QBqWLSr+j6Qu'/;9H-!Ej>XhPVU6J3E]\i:0Rkro(pPSm"``sUXC2J^\oqbphB_[]LLX)/8LtSViK.W/9@PFo+(eFcnW=E(R'9LL)2SBW@0mJ&-K9UEgLs(kb,V7LU2"#Y+VJ5/u@F*FSlV%=c4Q&Sil4]8mKF\!!K?fBoftJFa;?/_u)[1ima6@r&V"ebeaFd31e%J8ODMBZ\X=K<,,k^QLV@@[p'=Agu:oS2U7Ah\/6]EZ8=r59i4GG)gDFFs331X"4&uMFJiF?*&,C,_qEr239WqR'Loup071gY6p,0h"V4:]eedm]1c4"W,_`]Je:>uS'Pa!ji&N!b#?a?YL]T/AG"eeB<1nbro5-EN.aGfN'iY%=^dE]Vaq.3]Os9JRkGnXQ#8@PkE$EX-&+4#6JH~>endstream
endobj
120 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2689
>>
stream
Gatm>>ArQ1&q801Qq+&Wb8+YpS:dQgFHc)-l.2?boUp_c"X?l!'3S9`HF*GAB1Q;[g[_0K,>Md41W-bQj4H'*QYkTs-,A%5N,EI9#s?.6:TOjbgqra%ffs+m\T?b6?Jphki!6\JG,>p8LWaqhY%\'*$P,n7"+>K;6XZoYLs>c5j:_s9\1f;KLF\fD0%*CKH,;g<RcZ>g3J]M;HNulh:^`$H)3\$<bWuR*b;h[I_o#8RYV5cdK_#6Za`bMlSX+*C3$=7IJE9J<YoeMX]4<t-;38+`*uhh:X/SO(ik`ub5Ve.F6CRaNm[95T>:`-[!o;bq+b.(iY334$&98WDa^j/"27eJ5DD>`Mi#=21%3%B*WNNPZ\p]FDDG=N=cOYK$)1CQP=)U1&1S%XaKXg(WlDn0HA`4s(B:j@QYQI\5k6W-rmsc;4h`T-@Z#IgU/;TAT9mlC4*rk8`I9]Ve!DPu)G-Z)bZ%YF9.4d[""lNZ?Jj?HdPU.>&D,NFEGPaX5D[C9uF'?M)H+8^rl\I&Fr'iVOYrE"X`5:;Uc?iMo4%@2ppV(hJ^43I/k(>e7$kYcpeKUWXn+D]>fkCEL+^]T9nao&(rQ]AFe@Wh3Z94H0Cm?9-LaNjk]1LW:4J"p4`0L"E/CiHfa-/[)BKoO"#:A;nTX=+#W;Ml0XIc-Fh\bD^L*iE$3^JW]%B1gA/-uaA<FRf^j=21u]Xn[up:,E8\Y?grX1cAmO%j@PMQWIY^KsH409r9$S"O58%&qlM@Q-W.lrUGsdPRsl)/u_0PU=:1<`t4++^*eI4WNj.a&DAL37;#_$4V4.KTHOM+^_M&nam-qqYnC_0]=Yn7^_q&&^j'kF3-^Cj#B8"=d@H87!D\A_gJ?A/Ap5fQ;MkDh$MqY]E3tZFmJZrYJ''Dl?uu[][P6sKdd:AVH2?ONj-gA%MSQ<Bj_,o;m/O%lus6B8(QS?=cOkUbElJ=BYSDF-XGC@rgJ7!`4dU=fL#:Ur-:K4!od!\)nNfQ5+WqEH`^/qZ2l-F0nF\X1i=t*f017b2R^'\+G?\p[=V3)![=i!!^9TlLppOqpf&gfVJ7^@;memAG`[g'e1Q/D$WfPL_V'KM]LFPUIlFFHG72U[jnbZRG<EXERpN9?WqA"c/(]m<6Z`.=m;#pEhr'JG;Jt.O$jCn-aY,@#@&uT>csf;+7dWs-ORQ%F)FMRU*5TXHkE&:*bX-H_difZ%S7tWZp%710iBbRd]4J4<KPdcic_ZGMcW!O,>!Y*+%,IQdoah/<g%4gY3o:_-qoV!:0C(pWl@]NoYWi]IJ.6b#>Sf,f=U#8=AAqN#V.0,)cm>nHBF>7m:;aLjmu$h]eY:?6q3!)WBe,iK@=ElW-"@iK$iZeK>[S%d'.XC"U.V1&ad5u@6-<;?*FJn038"@D_ZDb8)AIU-Bn["mo8Ps+![K+;RBrnta:%r`;-^"dXiu>*BrYAG7+4L8`t0"UbhjSQ4@<3"M]HSo,-#>HAm&/9b5't<_.8PM#FS,-pkF;>lOe:iAgl3pD(&r+gX.VR$.^_kR@_EMH]$[QJ[iQY^V&#.U3(+n&NqH4#pLG2a<sDlm^JHCohW#]ZXKnH%+0po%?jA<pVtC#.:C[l-]^$62VkTom[n2JWi6(u>P/:3T>RRpVA_)0@F9S3$_mrJFGH\pV5h^2A+#HsBZTl::J4Bg7r>GQKWS,\*YTgg\Dg)5@YMcDM*A1D*MRd0rr/l'F(W'j\%.Z9a$boT+f=dJ'=fM@cg+]tRqTCOD@*U]U?V%eBY()M1nFRI:#_^/(\dUMh)h`\NujM`XgO2r;ol\(-#nb&NP/7->j8=1F*7'GD6e*V[Xh0P`,aH5-^&$I(6!Z/\^lTFLr=(t#T>S^WE2%!$bC2BgQJ5#r;AY]'\+0:_$9ne\YVnT5HfR!@(o.pK.4XA,L/NkOQqY@T^E(gPVc;?2f=kKoH/./3@J@oGdr+o>O-F8C`@]6.uUCPQL-,E9+p&dPf;6R1i+,n".cZ3(&+C.Y`BhXg`d8fKUZb5,nGup/g.#a9`A"@YB,5H<D61d`*TgB.kF36mP_00=4d'8_R]568??&:HdAM_r^J!WgY>_Yn2?"!_,J'Q/`H4o1&X6PR`rS+8$0d$liS-6Ge8_-gYD!pjji2'p-;iU'W^GSWNJE;Fqn`+0%)iE`eVH(D[f]m#D,RO+&fVh\8-'0D'5[]"t?)I;N`n.8K(-VKrg8<SI57VBjhm3rMg*@&%$EFWTB/&m4N'2$*[>V2g!=3Pi'O49d8g[M&7r_H>D&l^]GIeNGe5agrcso2U,@48[n)9Y`nK0rn2LeDsPV#K5m7@Jl/Kg)'Vk0QYN4V"8:J1((C2ja\+"Jrh+-i;u#g:)1;5WraC7niI1!oER2<N?s@fXaEETb/_R$+GH$F5T#O[;aNB+&Sp#5qoT.g\VYHS.4?!-hqjnr`iC\SnZ"ks]6(e60%7?GLPZqE/jeiQb`*P:NBMg/iMPQ*P"5;^*i**QJF`r,*ldQl^%c[kp4A\8O?18n>1@.sQT"\TVIXitE@!UL/@66oh%_mRPk-j`2rk?fknD*NIm(-%>_L+bk"=&"^^></qFa\I`r>"_r't'BbS0n'`I'qJ&gKO$:o/._U@(URl(O>`5k$Nt57I^D!gA\iem2\QIV>k7>;l^aNfRnj?B.=u@>W+-#Ashb>@4h'^A[V(t7pJ%N5Jq2*<r~>endstream
endobj
xref
0 121
0000000000 65535 f
0000000061 00000 n
0000000160 00000 n
0000000267 00000 n
0000000379 00000 n
0000000526 00000 n
0000000673 00000 n
0000000820 00000 n
0000000967 00000 n
0000001114 00000 n
0000001261 00000 n
0000001409 00000 n
0000001557 00000 n
0000001635 00000 n
0000001784 00000 n
0000001931 00000 n
0000002077 00000 n
0000002226 00000 n
0000002375 00000 n
0000002523 00000 n
0000002672 00000 n
0000002821 00000 n
0000002969 00000 n
0000003117 00000 n
0000003265 00000 n
0000003413 00000 n
0000003561 00000 n
0000003709 00000 n
0000003857 00000 n
0000004005 00000 n
0000004153 00000 n
0000004301 00000 n
0000004449 00000 n
0000004598 00000 n
0000004747 00000 n
0000004895 00000 n
0000005043 00000 n
0000005191 00000 n
0000005339 00000 n
0000005487 00000 n
0000005635 00000 n
0000005783 00000 n
0000005930 00000 n
0000006077 00000 n
0000006226 00000 n
0000006374 00000 n
0000006522 00000 n
0000006670 00000 n
0000006819 00000 n
0000006935 00000 n
0000007462 00000 n
0000007582 00000 n
0000007688 00000 n
0000007896 00000 n
0000008104 00000 n
0000008188 00000 n
0000008396 00000 n
0000008604 00000 n
0000008812 00000 n
0000009020 00000 n
0000009228 00000 n
0000009436 00000 n
0000009644 00000 n
0000009852 00000 n
0000009940 00000 n
0000010285 00000 n
0000010361 00000 n
0000010459 00000 n
0000010577 00000 n
0000010728 00000 n
0000010837 00000 n
0000010956 00000 n
0000011110 00000 n
0000011222 00000 n
0000011401 00000 n
0000011773 00000 n
0000012188 00000 n
0000012329 00000 n
0000012439 00000 n
0000012549 00000 n
0000012938 00000 n
0000013142 00000 n
0000013290 00000 n
0000013390 00000 n
0000013524 00000 n
0000013669 00000 n
0000013800 00000 n
0000013955 00000 n
0000014098 00000 n
0000014238 00000 n
0000014382 00000 n
0000014525 00000 n
0000014666 00000 n
0000014815 00000 n
0000014915 00000 n
0000015207 00000 n
0000015319 00000 n
0000015451 00000 n
0000015583 00000 n
0000015720 00000 n
0000015841 00000 n
0000015963 00000 n
0000016147 00000 n
0000016305 00000 n
0000016479 00000 n
0000016918 00000 n
0000017080 00000 n
0000017225 00000 n
0000017372 00000 n
0000017470 00000 n
0000017606 00000 n
0000019745 00000 n
0000023955 00000 n
0000027172 00000 n
0000031110 00000 n
0000035788 00000 n
0000040211 00000 n
0000043875 00000 n
0000047603 00000 n
0000051755 00000 n
0000056447 00000 n
trailer
<<
/ID
[<24e9fc955939835014159e55a7e21eb8><24e9fc955939835014159e55a7e21eb8>]
% ReportLab generated PDF document -- digest (opensource)
/Info 64 0 R
/Root 63 0 R
/Size 121
>>
startxref
59229
%%EOF

View File

@@ -6,6 +6,29 @@ Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha buil
---
---
## Table of contents
<!-- toc -->
- [Alpha scope (what we ship)](#alpha-scope-what-we-ship)
- [Recommended defaults (both devices)](#recommended-defaults-both-devices)
- [Feature freeze checklist (engineering)](#feature-freeze-checklist-engineering)
- [Alpha app sign-off (manual QA)](#alpha-app-sign-off-manual-qa)
- [A. Smoke (required — all must pass)](#a-smoke-required-all-must-pass)
- [B. Resilience (required)](#b-resilience-required)
- [C. Settings matrix (pick one row for official alpha demo)](#c-settings-matrix-pick-one-row-for-official-alpha-demo)
- [D. Regression guards (quick)](#d-regression-guards-quick)
- [E. Known limitations (not alpha blockers)](#e-known-limitations-not-alpha-blockers)
- [Log capture (when something fails)](#log-capture-when-something-fails)
- [Alpha backend (optional for LAN-only alpha)](#alpha-backend-optional-for-lan-only-alpha)
- [Ending feature freeze](#ending-feature-freeze)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Alpha scope (what we ship)
| In scope | Out of scope (frozen) |

View File

@@ -2,6 +2,23 @@
Manual QA on **two devices** on the same WiFi. Goal: stable show path without experimental transports or stream protection.
---
## Table of contents
<!-- toc -->
- [Before you start](#before-you-start)
- [Happy path (~15 min)](#happy-path-15-min)
- [Stress (optional, ~10 min)](#stress-optional-10-min)
- [Pass criteria](#pass-criteria)
- [If something fails](#if-something-fails)
- [Not in alpha soak scope](#not-in-alpha-soak-scope)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Before you start
| Setting | Sender | Receiver |

View File

@@ -7,6 +7,39 @@ Research and recommendations for **movie cast** vs **video cast** quality enhanc
---
---
## Table of contents
<!-- toc -->
- [0. Receiver A/V presets (product, May 2026)](#0-receiver-av-presets-product-may-2026)
- [1. User requirements (preliminary conditions)](#1-user-requirements-preliminary-conditions)
- [2. Mode overview](#2-mode-overview)
- [3. Video — method comparison](#3-video-method-comparison)
- [4. Audio — method comparison](#4-audio-method-comparison)
- [5. Single vs 2-frame vs stream — decision matrix](#5-single-vs-2-frame-vs-stream-decision-matrix)
- [6. PTS/DTS reassembly (2.3)](#6-ptsdts-reassembly-23)
- [7. Pipeline placement: sender (2.1) vs receiver (2.2)](#7-pipeline-placement-sender-21-vs-receiver-22)
- [User question](#user-question)
- [Assessment](#assessment)
- [Preference (consensus from session)](#preference-consensus-from-session)
- [8. Named algorithm pipelines (best practices)](#8-named-algorithm-pipelines-best-practices)
- [A. Receiver — live video cast (default)](#a-receiver-live-video-cast-default)
- [B. Receiver — movie cast (+1 frame delay)](#b-receiver-movie-cast-1-frame-delay)
- [C. Sender — optional compression prep only](#c-sender-optional-compression-prep-only)
- [D. Avoid for realtime in this project](#d-avoid-for-realtime-in-this-project)
- [9. Ranked preferences (quality benefit vs implementation in android cast)](#9-ranked-preferences-quality-benefit-vs-implementation-in-android-cast)
- [10. Multithreading pipeline (target architecture)](#10-multithreading-pipeline-target-architecture)
- [11. Short answers to numbered conditions](#11-short-answers-to-numbered-conditions)
- [12. Bottom line](#12-bottom-line)
- [13. Other work in same project session (context only)](#13-other-work-in-same-project-session-context-only)
- [14. Open decisions when implementation starts](#14-open-decisions-when-implementation-starts)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 0. Receiver A/V presets (product, May 2026)
Developer-only combo in **Developer settings** (not the main cast drawer):

View File

@@ -1,5 +1,22 @@
# Android Cast build ecosystem
---
## Table of contents
<!-- toc -->
- [Shared authentication](#shared-authentication)
- [Local stack](#local-stack)
- [Trigger a build](#trigger-a-build)
- [Docker CI](#docker-ci)
- [CircleCI](#circleci)
- [Production notes](#production-notes)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Shared authentication
All web consoles (Crashes, Tickets, Builder) share:

View File

@@ -2,6 +2,20 @@
Ads, in-app purchases, and Play Store helpers are **disabled for normal users**. Turn them on only in **Developer settings** while testing monetization.
---
## Table of contents
<!-- toc -->
- [Developer toggles](#developer-toggles)
- [Before production](#before-production)
- [Code map](#code-map)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Developer toggles
| Setting | Effect |

View File

@@ -1,5 +1,19 @@
# Anonymous crash reporter
---
## Table of contents
<!-- toc -->
- [Android](#android)
- [Backend](#backend)
- [Report files](#report-files)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Android
- **Main process:** `CrashReporter.install()` — any uncaught Java exception (cast or idle UI) → `files/crash_reports/pending/crash_yyyyMMdd_HHmmss.json` (same stamp as `session_*.json`). Active cast session stats are flushed with `globals.reason: "crash"` when applicable.

View File

@@ -13,6 +13,39 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a
---
---
## Table of contents
<!-- toc -->
- [1. Goals](#1-goals)
- [2. Branches](#2-branches)
- [3. Branch diagram (steady state)](#3-branch-diagram-steady-state)
- [4. Daily development](#4-daily-development)
- [5. Release: next → master](#5-release-next-master)
- [6. Hotfixes — chosen strategy](#6-hotfixes-chosen-strategy)
- [Default: hotfix from master (option 1) ✓](#default-hotfix-from-master-option-1)
- [Exception: fix only on next (option 2)](#exception-fix-only-on-next-option-2)
- [7. Protection rules (CI + policy)](#7-protection-rules-ci-policy)
- [7.1 How to protect branches (not local hooks alone)](#71-how-to-protect-branches-not-local-hooks-alone)
- [8. Sync cheat sheet](#8-sync-cheat-sheet)
- [9. Agent / Cursor checklist](#9-agent-cursor-checklist)
- [10. Comparison to classic Git Flow](#10-comparison-to-classic-git-flow)
- [11. First-time setup (maintainer)](#11-first-time-setup-maintainer)
- [12. Private primary + GitHub publish (dual remote)](#12-private-primary-github-publish-dual-remote)
- [12.1 Add GitHub without replacing origin](#121-add-github-without-replacing-origin)
- [12.2 What to push where](#122-what-to-push-where)
- [12.3 Daily habit](#123-daily-habit)
- [12.4 Where branch protection lives](#124-where-branch-protection-lives)
- [12.5 One-way mirror (optional automation)](#125-one-way-mirror-optional-automation)
- [12.6 Suggested order of operations (your situation)](#126-suggested-order-of-operations-your-situation)
- [Related](#related)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Goals
| Goal | How |
@@ -388,6 +421,7 @@ Avoid `git push github --mirror` unless GitHub is a full duplicate of **private*
## Related
- [README.md](README.md) — documentation index (all guides)
- [ROADMAP.md](ROADMAP.md) — product tracks
- [.cursor/rules/git-flow.mdc](../.cursor/rules/git-flow.mdc) — agent enforcement
- `docs/_pdf_build/build_git_flow_pdf.py` — PDF generator

View File

@@ -2,6 +2,28 @@
_Date: 2026-06-02_
---
## Table of contents
<!-- toc -->
- [Context and constraints](#context-and-constraints)
- [Executive summary](#executive-summary)
- [Option comparison](#option-comparison)
- [Role-based graph packs (suggested)](#role-based-graph-packs-suggested)
- [User graphs](#user-graphs)
- [Slug admin graphs](#slug-admin-graphs)
- [Platform admin graphs](#platform-admin-graphs)
- [Estimates (engineering)](#estimates-engineering)
- [Recommendation under your constraints](#recommendation-under-your-constraints)
- [Implementation slice (proposed)](#implementation-slice-proposed)
- [Source linkage](#source-linkage)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Context and constraints
- Stack: minimalistic LAMP with nginx + php-fpm + MariaDB.

View File

@@ -4,6 +4,7 @@
| Doc | When |
|-----|------|
| [docs/README.md](README.md) | **Documentation index** — TOC for all guides |
| **This file** | Topology, hosts, ports, sshfs, agent rules |
| [examples/app_hub/DEPLOY.md](../examples/app_hub/DEPLOY.md) | Hub landing only (BE `listen 80`) |
| [examples/crash_reporter/backend/README.md](../examples/crash_reporter/backend/README.md) | PHP, MariaDB, tickets, ingest |
@@ -12,6 +13,31 @@
---
---
## Table of contents
<!-- toc -->
- [1. Topology](#1-topology)
- [2. Port 8089 — not the androidcast vhost](#2-port-8089-not-the-androidcast-vhost)
- [3. Public URLs](#3-public-urls)
- [4. Disk layout (BE)](#4-disk-layout-be)
- [5. Nginx — source of truth in repo](#5-nginx-source-of-truth-in-repo)
- [BE apps.conf today (typical)](#be-appsconf-today-typical)
- [Hub location (correct)](#hub-location-correct)
- [Reload (BE only for hub deploy)](#reload-be-only-for-hub-deploy)
- [6. Local sshfs workspace (not in git)](#6-local-sshfs-workspace-not-in-git)
- [7. FE nginx — two server names](#7-fe-nginx-two-server-names)
- [8. Verify](#8-verify)
- [9. Backend app (crashes / tickets)](#9-backend-app-crashes-tickets)
- [10. Rules for agents](#10-rules-for-agents)
- [11. Changelog (infra doc)](#11-changelog-infra-doc)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Topology
```text

View File

@@ -2,6 +2,26 @@
Triage **real device crashes** vs **synthetic / bulk test traffic** when exposing upload or query APIs beyond the Android app. Builds on RBAC ([`examples/crash_reporter/backend/README.md`](../examples/crash_reporter/backend/README.md)).
---
## Table of contents
<!-- toc -->
- [Problems](#problems)
- [Payload extension (ingest)](#payload-extension-ingest)
- [Upload API (POST …/api/upload.php)](#upload-api-post-apiuploadphp)
- [Console / query API (read)](#console-query-api-read)
- [Classifying synthetic traffic (heuristics)](#classifying-synthetic-traffic-heuristics)
- [Security](#security)
- [Android](#android)
- [Triage tags (implemented v1)](#triage-tags-implemented-v1)
- [Implementation order](#implementation-order)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Problems
- Load tests and CI may POST many similar JSON blobs → drown signal in the reports list.

View File

@@ -6,6 +6,26 @@ The client runs checks and downloads in **`OtaUpdateService`** (a standalone `Se
MQTT is fully supported for OTA sources using retained topic payloads (`mqtt://...` / `mqtts://...`). The URI path is treated as the exact topic to subscribe.
---
## Table of contents
<!-- toc -->
- [URL layout](#url-layout)
- [Versioning](#versioning)
- [JSON files](#json-files)
- [Optional HTTP bundle (.otabundle.zip)](#optional-http-bundle-otabundlezip)
- [MQTT transport](#mqtt-transport)
- [Publish from a release APK](#publish-from-a-release-apk)
- [Docker build + staging channel](#docker-build-staging-channel)
- [App configuration](#app-configuration)
- [Checklist](#checklist)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## URL layout
```text

View File

@@ -2,6 +2,24 @@
Design notes for a commercial build without forking the cast protocol. **Not implemented yet** — complements [COMMERCIAL.md](COMMERCIAL.md) (dev-gated Play Billing placeholders).
---
## Table of contents
<!-- toc -->
- [Goals](#goals)
- [Entitlements API (in core)](#entitlements-api-in-core)
- [Module layout (Gradle)](#module-layout-gradle)
- [Play Billing flow](#play-billing-flow)
- [Build & release](#build-release)
- [Migration steps (suggested order)](#migration-steps-suggested-order)
- [Relation to sender resolution test](#relation-to-sender-resolution-test)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Goals
- **OSS (`cast-core`)** — protocol, UDP/TCP transport, receiver, sender engine, crash reporter client, settings model. Publishable repo for trust and forks.

100
docs/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Android Cast — documentation index
Click a link to open a guide. Section links jump within the document (GitHub, Cursor, and most Markdown previews).
---
## Table of contents
<!-- toc -->
- [All documents](#all-documents)
- [By topic](#by-topic)
- [Infrastructure and deploy](#infrastructure-and-deploy)
- [Development process](#development-process)
- [Alpha and QA](#alpha-and-qa)
- [Backend apps](#backend-apps)
- [Product and codecs](#product-and-codecs)
- [Agent bootstrap](#agent-bootstrap)
- [Maintaining TOCs](#maintaining-tocs)
<!-- /toc -->
---
## All documents
| Document | Summary |
|----------|---------|
| [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | On-demand remote access (WG/SSH/file API) proposals. |
| [ALPHA.md](ALPHA.md) | Alpha feature freeze, QA sign-off, defaults. |
| [ALPHA_SOAK.md](ALPHA_SOAK.md) | Short LAN soak checklist for demo validation. |
| [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) | AV enhancement Q&A: sender vs receiver pipelines. |
| [BUILD_DEPLOY.md](BUILD_DEPLOY.md) | Docker CI, staging OTA, builder console deploy. |
| [COMMERCIAL.md](COMMERCIAL.md) | Developer-gated ads / IAP toggles. |
| [CRASH_REPORTER.md](CRASH_REPORTER.md) | Anonymous crash upload (app + backend). |
| [GIT_FLOW.md](GIT_FLOW.md) | Branches (`master` / `next`), release, hotfix, dual remote. |
| [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) | Custom graphs vs Grafana decision memo. |
| [INFRA.md](INFRA.md) | Production topology: Gentoo FE → Alpine BE, ports, nginx, sshfs. |
| [OPEN_API.md](OPEN_API.md) | Crash ingest API design, triage tags, heuristics. |
| [OTA.md](OTA.md) | OTA channel layout v0, manifests, MQTT, publish flow. |
| [PRO_AAR.md](PRO_AAR.md) | cast-core / cast-pro AAR split and Play Billing. |
| [ROADMAP.md](ROADMAP.md) | Implementation roadmap, alpha definition, deferred work. |
| [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) | Tickets console, Gitea, attachments, agent queue. |
| [USB_HDMI_CAST.md](USB_HDMI_CAST.md) | Roadmap D/E: HDMI awareness, USB-tether transport. |
---
## By topic
### Infrastructure and deploy
- [INFRA.md](INFRA.md) — FE/BE topology, nginx, ports ([§1 Topology](INFRA.md#1-topology))
- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker CI, builder ([§Docker CI](BUILD_DEPLOY.md#docker-ci))
- [OTA.md](OTA.md) — OTA v0 layout ([§URL layout](OTA.md#url-layout))
### Development process
- [GIT_FLOW.md](GIT_FLOW.md) — `master` / `next` ([§2 Branches](GIT_FLOW.md#2-branches))
- [ROADMAP.md](ROADMAP.md) — milestones ([§Alpha definition](ROADMAP.md#alpha-definition))
### Alpha and QA
- [ALPHA.md](ALPHA.md) — freeze and sign-off ([§Alpha scope](ALPHA.md#alpha-scope-what-we-ship))
- [ALPHA_SOAK.md](ALPHA_SOAK.md) — LAN soak ([§Happy path](ALPHA_SOAK.md#happy-path-15-min))
### Backend apps
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
- [OPEN_API.md](OPEN_API.md) — ingest API ([§Upload API](OPEN_API.md#upload-api-post-apiv1uploadphp))
- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea
- [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs pivot
- [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — remote access ([§Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive))
### Product and codecs
- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP
- [PRO_AAR.md](PRO_AAR.md) — Pro AAR split
- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — HDMI / USB-tether
- [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) — AV enhancement Q&A
---
## Agent bootstrap
After a context reset, read in order:
1. [INFRA.md](INFRA.md) — topology (no `:8089` for androidcast)
2. [GIT_FLOW.md](GIT_FLOW.md) — branch rules
3. Task-specific: [BUILD_DEPLOY.md](BUILD_DEPLOY.md), [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md), [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md), etc.
Repo root [AGENTS.md](../AGENTS.md) points here.
---
## Maintaining TOCs
Section links use GitHub/Cursor-style heading anchors. After adding or renaming `##` / `###` headings, run:
```bash
python3 scripts/generate-docs-toc.py
```

View File

@@ -4,6 +4,30 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra
---
---
## Table of contents
<!-- toc -->
- [Alpha definition](#alpha-definition)
- [Alpha app checklist](#alpha-app-checklist)
- [Alpha backend checklist](#alpha-backend-checklist)
- [Product positioning](#product-positioning)
- [USB-C / HDMI (roadmap D) and USB-tether cast (roadmap E)](#usb-c-hdmi-roadmap-d-and-usb-tether-cast-roadmap-e)
- [Commercial (Play Store prep)](#commercial-play-store-prep)
- [Crash console open API](#crash-console-open-api)
- [Stream protection (FEC / NACK)](#stream-protection-fec-nack)
- [libvpx / Opus / Speex](#libvpx-opus-speex)
- [Transports & protocols](#transports-protocols)
- [Receiver / sender UX](#receiver-sender-ux)
- [Deferred / post-alpha](#deferred-post-alpha)
- [Related docs](#related-docs)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Alpha definition
| Track | Goal | Status |
@@ -162,8 +186,10 @@ See [ndk/README.md](../ndk/README.md).
## Related docs
- [README.md](README.md) — **documentation index** (all `docs/*.md` with section links)
- [GIT_FLOW.md](GIT_FLOW.md) — green `master` / `next` development cycle ([PDF](GIT_FLOW.pdf))
- [README.md](../README.md) — quick start
- [INFRA.md](INFRA.md) — production FE/BE topology
- [../README.md](../README.md) — repo quick start
- [ALPHA.md](ALPHA.md) — alpha freeze & QA sign-off
- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — D & E
- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP / Play
@@ -172,3 +198,6 @@ See [ndk/README.md](../ndk/README.md).
- [OTA.md](OTA.md) — backend layout v0
- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker build, staging OTA, deploy flow
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
- [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs dashboard pivot
- [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — on-demand remote access
- [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) — AV quality enhancement Q&A

View File

@@ -1,5 +1,22 @@
# Tickets, Gitea, and alpha smoke delegation
---
## Table of contents
<!-- toc -->
- [Gitea (/app/androidcast_project/git/)](#gitea-appandroidcastprojectgit)
- [Alpha smoke tickets (typical DB id → block)](#alpha-smoke-tickets-typical-db-id-block)
- [Console features (implementation)](#console-features-implementation)
- [Attachments (deploy)](#attachments-deploy)
- [Google Workspace — phase 2 (not implemented)](#google-workspace-phase-2-not-implemented)
- [Agent / engineering queue (from your list)](#agent-engineering-queue-from-your-list)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Gitea (`/app/androidcast_project/git/`)
Public URL should return Gitea HTML (assets under `/app/androidcast_project/git/assets/…`).

View File

@@ -2,6 +2,21 @@
Android Cast streams over **WiFi** (UDP/TCP/QUIC). USB-C→HDMI adapters use Androids **wired external display** stack, which is separate from the cast protocol.
---
## Table of contents
<!-- toc -->
- [Layer model](#layer-model)
- [Phase D — external display awareness & future capture](#phase-d-external-display-awareness-future-capture)
- [Phase E — USB-tether transport](#phase-e-usb-tether-transport)
- [Tablet + adapter checklist (e.g. Doogee Tab G6 Max → HY300)](#tablet-adapter-checklist-eg-doogee-tab-g6-max-hy300)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Layer model
| Layer | Mechanism | This app today |

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Build docs/20260602_REVERSE_SSH_proposals_summary.pdf from markdown source + heading TOC."""
from __future__ import annotations
from pathlib import Path
from md_to_pdf import build_pdf_from_markdown
ROOT = Path(__file__).resolve().parents[2]
SOURCE_MD = ROOT / "docs" / "20260602_REVERSE_SSH_proposals_summary.md"
OUT_PDF = ROOT / "docs" / "20260602_REVERSE_SSH_proposals_summary.pdf"
def build_pdf() -> None:
build_pdf_from_markdown(
SOURCE_MD,
OUT_PDF,
doc_subtitle="Generated from markdown — rev. 1 + rev. 2 + rev. 3 (2026-06-02)",
)
if __name__ == "__main__":
build_pdf()

View File

@@ -0,0 +1,48 @@
"""Shared markdown heading / slug helpers for docs PDF and TOC scripts."""
from __future__ import annotations
import re
TOC_START = "<!-- toc -->"
TOC_END = "<!-- /toc -->"
def github_slug(text: str) -> str:
"""GitHub/Cursor-compatible heading anchor."""
text = text.strip().lower()
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
text = re.sub(r"[*_]+", "", text)
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"\s+", "-", text)
text = re.sub(r"-+", "-", text)
return text.strip("-")
def collect_headings(lines: list[str]) -> list[tuple[int, str, str]]:
"""Return (level, title, slug) for ## and ### headings."""
out: list[tuple[int, str, str]] = []
used: dict[str, int] = {}
for line in lines:
m = re.match(r"^(#{2,3})\s+(.+?)\s*$", line)
if not m:
continue
level = len(m.group(1))
title = m.group(2).strip()
if title == "Table of contents":
continue
base = github_slug(title)
if not base:
continue
n = used.get(base, 0)
slug = base if n == 0 else f"{base}-{n}"
used[base] = n + 1
out.append((level, title, slug))
return out
def strip_md_link_text(title: str) -> str:
t = re.sub(r"\*\*([^*]+)\*\*", r"\1", title)
t = re.sub(r"`([^`]+)`", r"\1", t)
return t

View File

@@ -0,0 +1,318 @@
"""Render a markdown file to PDF with TOC from ## / ### headings (internal links + outline)."""
from __future__ import annotations
import re
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import (
Flowable,
PageBreak,
Paragraph,
Preformatted,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
from md_common import TOC_END, TOC_START, collect_headings, github_slug, strip_md_link_text
def _xml(text: str) -> str:
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
def _inline_md(text: str) -> str:
text = _xml(text)
text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
text = re.sub(r"`([^`]+)`", r'<font face="Courier" size="7">\1</font>', text)
text = re.sub(
r"\[([^\]]+)\]\(#([^)]+)\)",
r'<a href="#\2" color="#1565C0">\1</a>',
text,
)
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
return text
class AnchorParagraph(Paragraph):
"""Paragraph with PDF bookmark anchor."""
def __init__(self, text, style, *, anchor: str, outline_level: int):
super().__init__(f'<a name="{anchor}"/>{text}', style)
self._anchor = anchor
self._outline_level = outline_level
class OutlineDocTemplate(SimpleDocTemplate):
def afterFlowable(self, flowable: Flowable) -> None:
if isinstance(flowable, AnchorParagraph):
self.canv.bookmarkPage(flowable._anchor)
self.canv.addOutlineEntry(
flowable.getPlainText(),
flowable._anchor,
level=max(0, flowable._outline_level - 2),
)
def _cell(text: str, style: ParagraphStyle) -> Paragraph:
return Paragraph(_inline_md(text), style)
def _parse_table_row(line: str) -> list[str]:
line = line.strip()
if line.startswith("|"):
line = line[1:]
if line.endswith("|"):
line = line[:-1]
return [c.strip() for c in line.split("|")]
def _is_table_sep(line: str) -> bool:
return bool(re.match(r"^\|?[\s\-:|]+\|?$", line.strip()))
def build_slug_by_line(lines: list[str]) -> dict[int, str]:
"""Map source line index → anchor slug (same rules as collect_headings)."""
out: dict[int, str] = {}
used: dict[str, int] = {}
for i, line in enumerate(lines):
m = re.match(r"^(#{2,3})\s+(.+?)\s*$", line)
if not m:
continue
title = m.group(2).strip()
if title == "Table of contents":
continue
base = github_slug(title)
if not base:
continue
n = used.get(base, 0)
slug = base if n == 0 else f"{base}-{n}"
used[base] = n + 1
out[i] = slug
return out
def build_pdf_from_markdown(
source_md: Path,
out_pdf: Path,
*,
doc_title: str | None = None,
doc_subtitle: str = "",
) -> None:
lines = source_md.read_text(encoding="utf-8").splitlines()
headings = collect_headings(lines)
slug_by_line = build_slug_by_line(lines)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"DocTitle",
parent=styles["Title"],
fontSize=15,
spaceAfter=5,
textColor=colors.HexColor("#1565C0"),
)
toc_title = ParagraphStyle(
"TocTitle",
parent=styles["Heading1"],
fontSize=12,
spaceBefore=4,
spaceAfter=6,
textColor=colors.HexColor("#1565C0"),
)
toc_item = ParagraphStyle(
"TocItem",
parent=styles["Normal"],
fontSize=8,
leading=11,
leftIndent=0,
)
toc_sub = ParagraphStyle(
"TocSub",
parent=toc_item,
leftIndent=14,
fontSize=7.5,
leading=10,
)
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=11, spaceBefore=10, spaceAfter=4)
h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=9.5, spaceBefore=6, spaceAfter=3)
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=8, leading=10.5, alignment=TA_LEFT)
bullet = ParagraphStyle("Bullet", parent=body, leftIndent=12, bulletIndent=4, fontSize=8, leading=10.5)
small = ParagraphStyle("Small", parent=body, fontSize=7, textColor=colors.grey)
code_style = ParagraphStyle("Code", parent=body, fontName="Courier", fontSize=6.5, leading=8)
tbl_head = ParagraphStyle("TblHead", parent=body, fontName="Helvetica-Bold", fontSize=7, leading=9)
tbl_body = ParagraphStyle("TblBody", parent=body, fontSize=7, leading=9)
story: list = []
# Title from first # line if not overridden
if doc_title is None:
for line in lines:
if line.startswith("# "):
doc_title = line[2:].strip()
break
doc_title = doc_title or source_md.stem
story.append(Paragraph(_inline_md(doc_title), title_style))
if doc_subtitle:
story.append(Paragraph(_inline_md(doc_subtitle), small))
story.append(Spacer(1, 0.15 * cm))
# Table of contents (clickable internal links)
story.append(Paragraph("Table of contents", toc_title))
for level, title, slug in headings:
text = strip_md_link_text(title)
link = f'<a href="#{slug}" color="#1565C0">{_xml(text)}</a>'
style = toc_sub if level == 3 else toc_item
story.append(Paragraph(link, style))
story.append(Spacer(1, 0.2 * cm))
story.append(
Paragraph(
f"<i>Source: {source_md.name} — section links work in PDF viewers that support internal anchors.</i>",
small,
)
)
story.append(PageBreak())
# Body — skip title, TOC block, index link
skip_toc = False
in_code = False
code_lines: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
if line.strip() == TOC_START:
skip_toc = True
i += 1
continue
if line.strip() == TOC_END:
skip_toc = False
i += 1
continue
if skip_toc:
i += 1
continue
if line.startswith("# ") and not line.startswith("## "):
i += 1
continue
if line.strip() == "## Table of contents":
i += 1
continue
if line.strip().startswith("**Documentation index:**"):
i += 1
continue
if line.strip().startswith("```"):
if in_code:
story.append(Preformatted("\n".join(code_lines), code_style))
story.append(Spacer(1, 0.08 * cm))
code_lines = []
in_code = False
else:
in_code = True
i += 1
continue
if in_code:
code_lines.append(line)
i += 1
continue
if line.startswith("### "):
title = line[4:].strip()
slug = slug_by_line.get(i, github_slug(title))
story.append(
AnchorParagraph(_inline_md(title), h2, anchor=slug, outline_level=3)
)
i += 1
continue
if line.startswith("## "):
title = line[3:].strip()
slug = slug_by_line.get(i, github_slug(title))
story.append(
AnchorParagraph(_inline_md(title), h1, anchor=slug, outline_level=2)
)
i += 1
continue
if line.strip() == "---":
story.append(Spacer(1, 0.1 * cm))
i += 1
continue
if line.strip().startswith("|") and i + 1 < len(lines) and _is_table_sep(lines[i + 1]):
headers = _parse_table_row(line)
i += 2
rows: list[list[str]] = []
while i < len(lines) and lines[i].strip().startswith("|"):
rows.append(_parse_table_row(lines[i]))
i += 1
ncol = len(headers)
if ncol:
width = (16.2 * cm) / ncol
data = [[_cell(h, tbl_head) for h in headers]]
for row in rows:
padded = row + [""] * (ncol - len(row))
data.append([_cell(c, tbl_body) for c in padded[:ncol]])
t = Table(data, colWidths=[width] * ncol)
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 3),
("RIGHTPADDING", (0, 0), (-1, -1), 3),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]
)
)
story.append(t)
story.append(Spacer(1, 0.08 * cm))
continue
if line.strip().startswith("- "):
story.append(Paragraph(f"{_inline_md(line.strip()[2:])}", bullet))
i += 1
continue
if line.strip().startswith("_") and line.strip().endswith("_") and len(line.strip()) > 2:
story.append(Paragraph(f"<i>{_inline_md(line.strip()[1:-1])}</i>", small))
i += 1
continue
if not line.strip():
i += 1
continue
story.append(Paragraph(_inline_md(line.strip()), body))
i += 1
doc = OutlineDocTemplate(
str(out_pdf),
pagesize=A4,
leftMargin=1.4 * cm,
rightMargin=1.4 * cm,
topMargin=1.3 * cm,
bottomMargin=1.3 * cm,
title=doc_title,
author="Android Cast project",
)
doc.build(story)
print(f"Wrote {out_pdf} ({len(headings)} TOC entries from markdown headings)")

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
python3 "$ROOT_DIR/docs/_pdf_build/build_reverse_ssh_proposals_pdf.py"
echo "Generated: $ROOT_DIR/docs/20260602_REVERSE_SSH_proposals_summary.pdf"

View File

@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""Insert/update ## Table of contents in all docs/*.md (GitHub-style anchor links)."""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
TOC_START = "<!-- toc -->"
TOC_END = "<!-- /toc -->"
TOC_HEADING = "## Table of contents"
INDEX_NAME = "README.md"
# One-line blurbs for docs/README.md
DOC_BLURBS: dict[str, str] = {
"README.md": "Master index of all documentation (this file).",
"INFRA.md": "Production topology: Gentoo FE → Alpine BE, ports, nginx, sshfs.",
"GIT_FLOW.md": "Branches (`master` / `next`), release, hotfix, dual remote.",
"ROADMAP.md": "Implementation roadmap, alpha definition, deferred work.",
"ALPHA.md": "Alpha feature freeze, QA sign-off, defaults.",
"ALPHA_SOAK.md": "Short LAN soak checklist for demo validation.",
"BUILD_DEPLOY.md": "Docker CI, staging OTA, builder console deploy.",
"OTA.md": "OTA channel layout v0, manifests, MQTT, publish flow.",
"CRASH_REPORTER.md": "Anonymous crash upload (app + backend).",
"OPEN_API.md": "Crash ingest API design, triage tags, heuristics.",
"TICKETS_ROADMAP.md": "Tickets console, Gitea, attachments, agent queue.",
"COMMERCIAL.md": "Developer-gated ads / IAP toggles.",
"PRO_AAR.md": "cast-core / cast-pro AAR split and Play Billing.",
"USB_HDMI_CAST.md": "Roadmap D/E: HDMI awareness, USB-tether transport.",
"GRAFANA_vs_others_graphvis_pivot.md": "Custom graphs vs Grafana decision memo.",
"20260602_REVERSE_SSH_proposals_summary.md": "On-demand remote access (WG/SSH/file API) proposals.",
"AV_QUALITY_QA_SESSION.md": "AV enhancement Q&A: sender vs receiver pipelines.",
}
def github_slug(text: str) -> str:
"""Approximate GitHub/Cursor heading anchor slugs."""
text = text.strip().lower()
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
text = re.sub(r"[*_]+", "", text)
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"\s+", "-", text)
text = re.sub(r"-+", "-", text)
return text.strip("-")
def collect_headings(lines: list[str]) -> list[tuple[int, str, str]]:
"""Return (level, title, slug) for ## and ### only."""
out: list[tuple[int, str, str]] = []
used: dict[str, int] = {}
for line in lines:
m = re.match(r"^(#{2,3})\s+(.+?)\s*$", line)
if not m:
continue
level = len(m.group(1))
title = m.group(2).strip()
if title == "Table of contents":
continue
base = github_slug(title)
if not base:
continue
n = used.get(base, 0)
slug = base if n == 0 else f"{base}-{n}"
used[base] = n + 1
out.append((level, title, slug))
return out
def build_toc_block(headings: list[tuple[int, str, str]], *, doc_name: str) -> list[str]:
lines = [TOC_HEADING, "", TOC_START]
for level, title, slug in headings:
indent = " " if level == 3 else ""
# Strip markdown emphasis for link text
link_text = re.sub(r"\*\*([^*]+)\*\*", r"\1", title)
link_text = re.sub(r"`([^`]+)`", r"\1", link_text)
lines.append(f"{indent}- [{link_text}](#{slug})")
lines.append(TOC_END)
if doc_name != INDEX_NAME:
lines.append("")
lines.append(f"**Documentation index:** [{INDEX_NAME}]({INDEX_NAME})")
lines.append("")
return lines
def strip_existing_toc(lines: list[str]) -> list[str]:
out: list[str] = []
i = 0
while i < len(lines):
if lines[i].strip() == TOC_HEADING:
i += 1
while i < len(lines) and TOC_END not in lines[i]:
i += 1
if i < len(lines):
i += 1 # skip <!-- /toc -->
while i < len(lines) and lines[i].strip() == "":
i += 1
if i < len(lines) and lines[i].strip().startswith("**Documentation index:**"):
i += 1
while i < len(lines) and lines[i].strip() == "":
i += 1
continue
out.append(lines[i])
i += 1
return out
def find_insert_index(lines: list[str]) -> int:
"""Index to insert TOC block (before first ## section)."""
for i, line in enumerate(lines):
if re.match(r"^## (?!Table of contents)", line):
return i
return len(lines)
def process_file(path: Path) -> bool:
text = path.read_text(encoding="utf-8")
lines = text.splitlines(keepends=True)
raw = [ln.rstrip("\n") for ln in lines]
raw = strip_existing_toc(raw)
headings = collect_headings(raw)
if not headings and path.name != INDEX_NAME:
return False
toc = build_toc_block(headings, doc_name=path.name)
idx = find_insert_index(raw)
# Avoid duplicate --- before TOC
if idx > 0 and raw[idx - 1].strip() == "---":
new_lines = raw[: idx - 1] + toc + ["---", ""] + raw[idx:]
else:
sep = ["---", ""] if idx < len(raw) and raw[idx].startswith("##") else []
new_lines = raw[:idx] + toc + sep + raw[idx:]
new_text = "\n".join(new_lines) + "\n"
if new_text != text.replace("\r\n", "\n"):
path.write_text(new_text, encoding="utf-8")
return True
return False
def build_index_md() -> str:
md_files = sorted(p for p in DOCS.glob("*.md") if p.name != INDEX_NAME)
lines = [
"# Android Cast — documentation index",
"",
"Click a link to open a guide. Section links jump within the document (GitHub, Cursor, and most Markdown previews).",
"",
TOC_HEADING,
"",
TOC_START,
"- [All documents](#all-documents)",
"- [By topic](#by-topic)",
"- [Agent bootstrap](#agent-bootstrap)",
TOC_END,
"",
"---",
"",
"## All documents",
"",
"| Document | Summary |",
"|----------|---------|",
]
for p in md_files:
name = p.name
blurb = DOC_BLURBS.get(name, "")
lines.append(f"| [{name}]({name}) | {blurb} |")
lines += [
"",
"---",
"",
"## By topic",
"",
"### Infrastructure and deploy",
"",
f"- [INFRA.md](INFRA.md) — FE/BE topology, nginx, ports ([§1 Topology](INFRA.md#1-topology))",
f"- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker CI, builder ([§Docker CI](BUILD_DEPLOY.md#docker-ci))",
f"- [OTA.md](OTA.md) — OTA v0 layout ([§URL layout](OTA.md#url-layout))",
"",
"### Development process",
"",
f"- [GIT_FLOW.md](GIT_FLOW.md) — `master` / `next` ([§2 Branches](GIT_FLOW.md#2-branches))",
f"- [ROADMAP.md](ROADMAP.md) — milestones ([§Alpha definition](ROADMAP.md#alpha-definition))",
"",
"### Alpha and QA",
"",
f"- [ALPHA.md](ALPHA.md) — freeze and sign-off ([§Alpha scope](ALPHA.md#alpha-scope-what-we-ship))",
f"- [ALPHA_SOAK.md](ALPHA_SOAK.md) — LAN soak ([§Happy path](ALPHA_SOAK.md#happy-path-15-min))",
"",
"### Backend apps",
"",
f"- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload",
f"- [OPEN_API.md](OPEN_API.md) — ingest API ([§Upload API](OPEN_API.md#upload-api-post-apiv1uploadphp))",
f"- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea",
f"- [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs pivot",
f"- [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — remote access ([§Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive))",
"",
"### Product and codecs",
"",
f"- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP",
f"- [PRO_AAR.md](PRO_AAR.md) — Pro AAR split",
f"- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — HDMI / USB-tether",
f"- [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) — AV enhancement Q&A",
"",
"---",
"",
"## Agent bootstrap",
"",
"After a context reset, read in order:",
"",
"1. [INFRA.md](INFRA.md) — topology (no `:8089` for androidcast)",
"2. [GIT_FLOW.md](GIT_FLOW.md) — branch rules",
"3. Task-specific: [BUILD_DEPLOY.md](BUILD_DEPLOY.md), [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md), [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md), etc.",
"",
"Repo root [AGENTS.md](../AGENTS.md) points here.",
"",
"---",
"",
"## Maintaining TOCs",
"",
"Section links use GitHub/Cursor-style heading anchors. After adding or renaming `##` / `###` headings, run:",
"",
"```bash",
"python3 scripts/generate-docs-toc.py",
"```",
"",
]
return "\n".join(lines) + "\n"
def main() -> None:
index_path = DOCS / INDEX_NAME
index_path.write_text(build_index_md(), encoding="utf-8")
print(f"Wrote {index_path}")
changed = []
for path in sorted(DOCS.glob("*.md")):
if process_file(path):
changed.append(path.name)
print(f"Updated TOC: {path.name}")
print(f"Done. {len(changed)} file(s) updated.")
if __name__ == "__main__":
main()