GingerCal 1.4: bundled TLS 1.2, no bridge needed

CalDAV now goes through Minamp's own HTTP/TLS client (Bouncy Castle TLS 1.2, Mozilla
roots, keep-alive) instead of Apache HttpClient on Android 2.3's TLS 1.0, as the iPod
version does with BearSSL. The phone reaches the normal https:// Nextcloud address at
home and away; a pinned self-signed server or the old bridge still works.

- NetHttp transport; requests run on big-stack threads (the handshake overflows
  AsyncTask/IntentService stacks on Android 2.x)
- http:// -> https:// redirects on the same host are followed; other hosts still refused
- bridge wording replaced in Galician and English; README updated
- NetTest (TLS + CalDAV discovery through the new transport) replaces BridgeTest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XoséandClaude Opus 5.5 committed 2026-10-17 19:14:06 +02:00
1 parent 42c3d6da91
commit f7428287eb
145 files changed
+1278 -407

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
package org.gingercal;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import org.gingercal.net.CertVerifier;
/**
* Android 2.3 transport through the bundled TLS 1.2 client (org.gingercal.net, from Minamp):
* the phone's own TLS 1.0 can't reach Nextcloud, so this talks to the normal https:// address
* directly, any method (PROPFIND/REPORT included), with keep-alive. A pinned certificate (a
* self-signed server or an old bridge) is still honoured.
*/
public class NetHttp implements Http {
/** AsyncTask and IntentService threads have Android 2.x's small default stack, which the
* TLS handshake overflows: requests run here, on big-stack threads, and the caller waits. */
private static final ExecutorService NET = org.gingercal.net.Threads.pool("net", 3);
final String pin;
final int timeoutMs;
public NetHttp(String pin, int timeoutMs) {
this.pin = pin == null ? "" : pin;
this.timeoutMs = timeoutMs;
}
public Resp exec(final String method, final String url, final String[] headers, final String body,
final String contentType) throws IOException {
try {
return NET.submit(new Callable<Resp>() {
public Resp call() throws IOException {
return execHere(method, url, headers, body, contentType);
}
}).get();
} catch (ExecutionException e) {
Throwable c = e.getCause();
if (c instanceof IOException) throw (IOException) c;
if (c instanceof RuntimeException) throw (RuntimeException) c;
if (c instanceof Error) throw (Error) c;
throw new IOException(String.valueOf(c));
} catch (InterruptedException e) {
throw new IOException(L.t("Interrupted"));
}
}
Resp execHere(String method, String url, String[] headers, String body, String contentType)
throws IOException {
CertVerifier.setPin(pin);
Map<String, String> h = new LinkedHashMap<String, String>();
for (int i = 0; i + 1 < headers.length; i += 2) h.put(headers[i], headers[i + 1]);
byte[] data = null;
if (body != null) {
data = body.getBytes("UTF-8");
if (contentType != null) h.put("Content-Type", contentType);
}
org.gingercal.net.Http.Response r;
try {
// one hop at a time: Dav decides which redirects are safe to follow
r = org.gingercal.net.Http.requestNoRedirect(method, url, h, data, timeoutMs);
} catch (CertVerifier.Untrusted u) {
throw new DavException.UntrustedCert(u.fingerprint, u.subject, u.issuer, u.expires, u.changed);
}
Resp out = new Resp();
out.code = r.code;
out.etag = r.header("etag");
out.location = r.header("location");
out.body = r.readString();
return out;
}
public void close() {
// pooled connections are shared and expire by themselves
}
}