202 lines
12 KiB
Markdown
202 lines
12 KiB
Markdown
# 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.
|