158 lines
8.8 KiB
Markdown
158 lines
8.8 KiB
Markdown
# GingerCal — 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, reached through Pangolin.
|
||
|
||
**What it does**
|
||
|
||
- Agenda of upcoming events grouped by day, colour-coded per calendar, cached offline (SQLite)
|
||
- 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
|
||
- Certificate pinning, so a self-signed "legacy" endpoint 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 breaks on 2.3, and the fix
|
||
|
||
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 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 `GingerCal.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 GingerCal, 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: Sync, New event, Calendars, Settings. Tap an event for details, Edit and Delete.
|
||
|
||
If installation fails, check the file arrived whole (53 KB; 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 GingerCal.apk` prints the exact reason (`INSTALL_FAILED_...`).
|
||
|
||
## Building
|
||
|
||
Needs JDK 11+, Python 3 with Pillow, `d8` and `apksigner.jar` (Android SDK build-tools), and
|
||
`apktool.jar` (2.9+, which bundles Google's `aapt` and smali). 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 test/org/gingercal/*.java
|
||
java -cp out org.gingercal.CoreTest # discovery, sync, recurrence, create/edit/delete
|
||
java -cp out org.gingercal.RedirectTest # forced-HTTPS / redirect-loop handling (no server needed)
|
||
```
|
||
|
||
`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, Apache HttpClient transport
|
||
- `stubs/` compile-only Android API 10 signatures (never packaged)
|
||
- `bridge/` nginx legacy-TLS bridge: config, compose file, certificate script
|
||
- `res/` app name; the icon is drawn at build time (mdpi + ldpi for the Galaxy Mini)
|
||
- `tools/` icon, dex disassembler helper, zipalign, dex/API checkers, d8 workaround
|
||
|
||
## Security notes
|
||
|
||
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.
|