Files

202 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Calendar Lite — Nextcloud CalDAV for Android 2.3 (Galaxy Mini)
A small native calendar client for Gingerbread (API 9/10). Built for a Samsung Galaxy Mini
(GT-S5570, 240×320, Android 2.3.5) talking to Nextcloud at its normal `https://` address.
**Since 1.4 it brings its own TLS 1.2** (Bouncy Castle, the same client as Plexamp Lite, with Mozilla's
root certificates and keep-alive connections), like the iPod version does with BearSSL. Put the
normal `https://` Nextcloud address in Settings, at home or away: no bridge is needed. The bridge
sections below are kept for reference; a bridge address (pinned certificate) still works.
**What it does**
- Month view: a 6-week grid with a coloured mark per event (calendar colour), today outlined,
the selected day's events listed below. Swipe or tap the arrows to change month; tap the
month name for today; long-press a day to create an event on it. Months outside the synced
window are fetched from the server when you open them
- Agenda view: upcoming events grouped by day (Menu > View switches; the choice is remembered)
- Interface in Galician (default) or English: Settings > Lingua / Language. Month and weekday
names are built in, since Android 2.3 has no Galician locale data; in Galician weeks start
on Monday. The system date/time pickers follow the phone's own language
- Offline cache (SQLite) of the last 35 days and the next 60 (Settings: "Days ahead")
- Create, edit and delete events (title, all-day, start/end, location, notes, alarm)
- Repeating events are shown correctly (server-side expansion, with a client-side fallback);
they can be deleted on the phone but edited only in Nextcloud
- Pick which calendars to show and which one new events go to
- Background sync every 1–24 h, plus phone notifications N minutes before timed events
- Its own TLS 1.2 stack: connects to modern `https://` servers directly
- Certificate pinning, so a self-signed server (or the old legacy-TLS bridge) is still safe against interception
It does not feed the stock Samsung calendar: 2.3 has no public calendar-provider API for that.
## Why HTTPS broke on 2.3, and the old fix (not needed since 1.4)
Three separate problems, all fatal for a normal Pangolin/Traefik URL:
| Problem | Gingerbread | Pangolin (Traefik) |
|---|---|---|
| TLS version | TLS 1.0 max | TLS 1.2 minimum |
| Root CA | no ISRG Root X1 (Let's Encrypt) | Let's Encrypt certificates |
| SNI | unreliable in Apache HttpClient | routes HTTPS by SNI |
Lowering Traefik's TLS settings would weaken every site you host and still leave the CA problem.
Instead, put a tiny **legacy-TLS bridge** next to Nextcloud and expose it with a Pangolin
**raw TCP resource** (port-based, so no SNI needed). The bridge speaks TLS 1.0 with a
self-signed certificate that the app pins by fingerprint, and forwards **only** `/remote.php/dav/`
to Nextcloud.
```
Galaxy Mini --TLS 1.0, pinned cert--> Pangolin VPS :8443 (raw TCP, WireGuard tunnel)
--> newt --> caldav-bridge (nginx) --HTTP--> Nextcloud (CalDAV only)
```
### Option A: bridge through Pangolin (works anywhere)
1. **Bridge** (on the Nextcloud host, in `bridge/`):
- Edit `nginx.conf`: the `server nextcloud:80;` upstream and `set $nc_host` (a name in
Nextcloud's `trusted_domains`). If Nextcloud forces HTTPS, also switch to `proxy_pass https://`
(see "Forced HTTPS / HSTS" below).
- Edit `docker-compose.yml`: the Docker network your Nextcloud container is on.
- `./make-cert.sh` creates the certificate and prints its SHA-256 fingerprint. Note it.
- `docker compose up -d`
2. **Pangolin server** (VPS):
- `config/config.yml`: `flags: allow_raw_resources: true`
- `docker-compose.yml`, `gerbil` service: add port `8443:8443`
- `config/traefik/traefik_config.yml`, `entryPoints`: add
`tcp-8443: { address: ":8443/tcp" }` (the `tcp-<port>` name is required)
- Open TCP 8443 in the VPS firewall, then `docker compose down && docker compose up -d`
3. **Pangolin dashboard**: Resources, Add Resource, **Raw TCP/UDP**, TCP, public port **8443**,
target = the bridge as seen from your newt site (`caldav-bridge:8443` if newt shares its Docker
network, otherwise `127.0.0.1:8443` with the port published). Leave Proxy Protocol off.
4. **Phone**: server address `https://<your-pangolin-domain-or-IP>:8443`. On the first test
the app shows the certificate fingerprint; tap **Trust** only if it matches step 1.
Pangolin's SSO does not apply to raw TCP resources; protection here is Nextcloud's login (use an
app password) plus the pinned certificate. The web UI isn't reachable through the bridge.
Optional, to let Nextcloud's brute-force protection see real client IPs: tick **Enable Proxy
Protocol** on the resource, add the `pp-transport-v1/v2` block to Pangolin's
`dynamic_config.yml` (see the Pangolin raw-resources docs), and in `nginx.conf` use
`listen 8443 ssl proxy_protocol;`, `set_real_ip_from <newt address>;`,
`real_ip_header proxy_protocol;`. Enable both sides together or every connection fails.
### Option C: Nginx Proxy Manager with port forwarding (no Pangolin)
If your router already forwards 80/443 to Nginx Proxy Manager, the legacy endpoint can live in
NPM itself on its own port. NPM's normal sites on 443 keep their modern TLS settings.
1. `./bridge/make-cert.sh nxc.anosanube.ovh <npm>/data/legacy-caldav`, and note the fingerprint.
2. Copy `bridge/npm/http.conf` to `<npm>/data/nginx/custom/http.conf`; if that file already
exists, append the block to it. NPM includes it at the end of its `http` block.
3. In NPM's `docker-compose.yml` add `- '8443:8443'` under `ports`, then `docker compose up -d`.
4. Forward TCP 8443 on the router to the NPM host.
5. Phone: `https://nxc.anosanube.ovh:8443`, then tap Trust if the fingerprint matches.
It forwards to NPM's own `nxc.anosanube.ovh` proxy host over loopback HTTPS, so Nextcloud's
backend address, HSTS and forced HTTPS need no extra handling. Check from a PC with
`openssl s_client -connect nxc.anosanube.ovh:8443 -tls1 -cipher 'AES128-SHA:@SECLEVEL=0'`.
At home, the phone needs your router to support NAT loopback (it already does if
`https://nxc.anosanube.ovh` works from your Wi-Fi).
### Option B: home Wi-Fi only (no Pangolin)
Use `http://<nextcloud-LAN-IP>` as the server address. Nothing to set up, but it syncs only at
home and the password crosses your LAN unencrypted. The app warns if `http://` points at a
non-LAN address.
Avoid a plain-HTTP Pangolin resource: it would send your password in clear over the internet.
### Forced HTTPS / HSTS
**HSTS itself doesn't matter to the app.** `Strict-Transport-Security` is enforced by browsers;
the app's HTTP client ignores it, and a Pangolin raw TCP resource passes bytes through without
adding headers. The bridge strips the header anyway, so a modern browser that you point at
the bridge (e.g. while debugging) doesn't lock itself to the self-signed certificate.
**What does matter is a Nextcloud that redirects plain HTTP to its public `https://` URL**
(web server rule, `overwriteprotocol`, `overwrite.cli.url`). Symptoms in the app:
| App says | Cause | Fix |
|---|---|---|
| "Redirect loop … switch the bridge to proxy_pass https://" | bridge talks HTTP to a Nextcloud that redirects HTTP→HTTPS | in `nginx.conf`: `server nextcloud:443;` and `proxy_pass https://nextcloud;` |
| "The server redirected http:// to https://… (forces HTTPS / HSTS)" | Option B: phone talks `http://` to that Nextcloud on the LAN | use the bridge address, or let the LAN vhost serve `/remote.php/dav/` without the redirect |
| "The server redirected to https://cloud…, not the address set in the app" | an absolute redirect wasn't rewritten | keep the `proxy_redirect` and `absolute_redirect off` lines in `nginx.conf` |
With `proxy_pass https://` only the bridge-to-Nextcloud hop uses HTTPS (modern TLS, any
certificate, since it's your own server). The phone side stays TLS 1.0 with the pinned
certificate. If the bridge can't reach Nextcloud directly, `proxy_pass` can also point at the
public Pangolin URL. In that case Pangolin authentication must let `/remote.php/dav/` through, and
you should turn `proxy_ssl_verify` on.
## Install on the phone
1. Settings, Applications, tick **Unknown sources**.
2. Copy `CalendarLite.apk` to the SD card (USB) and open it with the file manager, or download it
over plain `http://` (the 2.3 browser can't open most HTTPS sites either).
3. Open Calendar Lite, Menu, Settings. In Nextcloud create an app password: Settings, Security,
Devices & sessions. Fill in server, user, app password, then **Save and test connection**.
Menu key: View (Month/Agenda), New event, Sync, Today, Calendars, Settings. Tap an event for
details, Edit and Delete.
If installation fails, check the file arrived whole (about 1 MB; some browsers save a truncated or
renamed `.apk.zip`) and that there are a few MB free on the internal storage. On a PC with
`adb`, `adb install CalendarLite.apk` prints the exact reason (`INSTALL_FAILED_...`).
## Building
Needs JDK 11+, Python 3, `d8` and `apksigner.jar` (Android SDK build-tools),
`apktool.jar` (2.9+, which bundles Google's `aapt` and smali), and the Bouncy Castle sources renamed
to `com.minamp.bc` that Plexamp Lite's `tools/setup_toolchain.sh` prepares (`BC_SRC`, default
`~/android-toolchain/bcsrc`). No Gradle.
```sh
D8=d8 APKTOOL=~/apktool.jar APKSIGNER=$SDK/build-tools/34.0.0/lib/apksigner.jar ./build.sh
# add ANDROID_JAR=$SDK/platforms/android-10/android.jar to compile against the real platform
```
Pipeline: javac → d8 → baksmali/smali (clean dex 035) → `aapt` v1 for manifest and resources →
`apksigner` v1 signature → zipalign. The script ends by running `apksigner verify` for API 8–10
and checking the dex header, and fails rather than produce an APK that 2.3 would reject.
Don't sign with `jarsigner`: modern versions add PKCS#7 signed attributes, which Android only
accepts from 4.4, so the phone refuses to install it. That was the bug in the first build.
`gingercal.keystore` (password `android`) is the signing key; keep using it so updates install
over the existing app.
Tests (desktop JVM, need a CalDAV server such as Radicale on :5232 with user `xose`/`secret`):
```sh
javac -d out src/org/gingercal/*.java $(ls test/org/gingercal/*.java | grep -v NetTest)
java -cp out org.gingercal.CoreTest # discovery, sync, recurrence, create/edit/delete
java -cp out org.gingercal.RedirectTest # redirect handling (no server needed)
# the bundled TLS transport (needs the android.jar and Bouncy Castle sources to compile):
# java -cp out org.gingercal.NetTest assets/roots http://127.0.0.1:5232 xose secret
java -cp out org.gingercal.I18nTest # Galician dates, titles and messages
```
`tools/check_i18n.py` (run by the build) fails if any interface text lacks a Galician
translation or the placeholders differ. `tools/check_api.py` verifies every framework call in the dex against published Android API
declarations.
## Layout
- `src/` CalDAV core, platform-neutral: discovery, PROPFIND/REPORT, iCalendar, sync, pinning
- `android/` activities, sync service, alarms, SQLite cache, `NetHttp` transport
- `android/org/gingercal/net/` the bundled TLS 1.2 HTTP client from Plexamp Lite; `assets/roots/` Mozilla roots
- `stubs/` compile-only Android API 10 signatures (never packaged)
- `bridge/` legacy-TLS endpoint: standalone nginx (config, compose file), `npm/http.conf` for
Nginx Proxy Manager, certificate script
- `res/` app name, launcher icon (ldpi to xhdpi; master in `art/`) and `stat_notify`, the white status-bar icon for reminders
- `tools/` dex disassembler helper, zipalign, dex/API checkers, d8 workaround
## Security notes
With the bundled TLS 1.2 the connection to Nextcloud is checked like a browser would: host name,
dates, and a chain up to a Mozilla root. The notes below are about the old bridge.
TLS 1.0 with AES-CBC is old but, combined with a pinned certificate, still keeps the connection
private and authenticated against anyone without the bridge's private key. The bridge exposes
only the DAV endpoint. Use an app password so you can revoke the phone from Nextcloud. If the
phone ever reports that the certificate changed and you didn't regenerate it, don't trust it.