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:
1 parent
42c3d6da91
commit
f7428287eb
145 files changed
+1278
-407
No files matched your search
@@ -1,99 +0,0 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import org.gingercal.Model.Cal;
|
||||
|
||||
/** Talks to the legacy bridge the way Android 2.3 does: TLS 1.0, AES128-SHA, pinned cert. */
|
||||
public class BridgeTest {
|
||||
static int fails;
|
||||
|
||||
static void check(boolean ok, String what) {
|
||||
System.out.println((ok ? " ok " : " FAIL ") + what);
|
||||
if (!ok) fails++;
|
||||
}
|
||||
|
||||
static class GingerbreadHttp implements Http {
|
||||
final Tls.PinTrust trust;
|
||||
final HttpClient c;
|
||||
|
||||
GingerbreadHttp(String pin) throws Exception {
|
||||
trust = new Tls.PinTrust(pin);
|
||||
SSLContext ctx = SSLContext.getInstance("TLSv1");
|
||||
ctx.init(null, new TrustManager[] {trust}, null);
|
||||
SSLParameters p = new SSLParameters(new String[] {"TLS_RSA_WITH_AES_128_CBC_SHA"}, new String[] {"TLSv1"});
|
||||
c = HttpClient.newBuilder().sslContext(ctx).sslParameters(p).build();
|
||||
}
|
||||
|
||||
public Resp exec(String method, String url, String[] h, String body, String ct) throws IOException {
|
||||
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(url)).method(method,
|
||||
body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(body));
|
||||
for (int i = 0; i + 1 < h.length; i += 2) b.header(h[i], h[i + 1]);
|
||||
if (ct != null) b.header("Content-Type", ct);
|
||||
trust.reset();
|
||||
try {
|
||||
HttpResponse<String> r = c.send(b.build(), HttpResponse.BodyHandlers.ofString());
|
||||
Resp o = new Resp();
|
||||
o.code = r.statusCode();
|
||||
o.body = r.body();
|
||||
o.etag = r.headers().firstValue("ETag").orElse(null);
|
||||
o.location = r.headers().firstValue("Location").orElse(null);
|
||||
return o;
|
||||
} catch (IOException e) {
|
||||
throw trust.translate(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {}
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
String url = "https://127.0.0.1:8443";
|
||||
String expected = a[0];
|
||||
// 1. first contact: not trusted -> app shows fingerprint
|
||||
try {
|
||||
new Dav(new GingerbreadHttp(""), url, "xose", "secret").calendarHome();
|
||||
check(false, "untrusted cert reported");
|
||||
} catch (DavException.UntrustedCert u) {
|
||||
check(u.fingerprint.equals(expected) && !u.pinMismatch, "untrusted cert reported with fingerprint " + u.fingerprint.substring(0, 11) + "...");
|
||||
}
|
||||
// 2. wrong pin -> mismatch warning
|
||||
try {
|
||||
new Dav(new GingerbreadHttp(expected.replace('1', '2')), url, "xose", "secret").calendarHome();
|
||||
check(false, "pin mismatch reported");
|
||||
} catch (DavException.UntrustedCert u) {
|
||||
check(u.pinMismatch, "pin mismatch reported");
|
||||
}
|
||||
// 3. pinned: full flow through the bridge
|
||||
Dav dav = new Dav(new GingerbreadHttp(expected), url, "xose", "secret");
|
||||
String home = dav.calendarHome();
|
||||
check(home.equals(url + "/remote.php/dav/xose/"), "discovered home via bridge: " + home);
|
||||
List<Cal> cals = dav.calendars(home);
|
||||
check(cals.size() == 2, "calendars via bridge: " + cals.size());
|
||||
CoreTest.MemStore st = new CoreTest.MemStore();
|
||||
Syncer.Result r = Syncer.sync(dav, st, System.currentTimeMillis(), 1, 60, false);
|
||||
check(r.events > 0, "sync via bridge: " + r.summary());
|
||||
String uid = UUID.randomUUID().toString();
|
||||
long s = ICal.startOfDay(System.currentTimeMillis()) + 2 * ICal.DAY + 9 * ICal.HOUR;
|
||||
Cal w = cals.get(0);
|
||||
dav.create(w.href, uid, ICal.newEvent(uid, "Via bridge", "", "", s, s + ICal.HOUR, false, 10));
|
||||
String[] got = dav.get(w.href + uid + ".ics");
|
||||
check(ICal.parse(got[0]).get(0).summary.equals("Via bridge"), "create+get via bridge");
|
||||
dav.delete(w.href + uid + ".ics", got[1]);
|
||||
check(true, "delete via bridge");
|
||||
System.out.println(fails == 0 ? "ALL PASSED" : fails + " FAILED");
|
||||
System.exit(fails == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class I18nTest {
|
||||
Dav.checkRedirect("http://192.168.1.20/remote.php/dav/", "https://nxc.anosanube.ovh/remote.php/dav/",
|
||||
new java.util.ArrayList<String>(), 301);
|
||||
} catch (DavException e) {
|
||||
check(e.getMessage().substring(0, 58), "O servidor redirixiu http:// a https://nxc.anosanube.ovh (");
|
||||
check(e.getMessage().substring(0, 44), "O servidor redirixiu a https://nxc.anosanube");
|
||||
}
|
||||
check(L.t("not in the table"), "not in the table"); // unknown text falls back to English
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.gingercal.net.CertVerifier;
|
||||
|
||||
/**
|
||||
* The phone's transport (NetHttp: bundled TLS 1.2 client, any method, keep-alive) on a desktop JVM:
|
||||
* CalDAV discovery against a local Radicale, and certificate checks against real HTTPS servers.
|
||||
* java -cp out org.gingercal.NetTest assets/roots [http://127.0.0.1:5232 user pass]
|
||||
*/
|
||||
public class NetTest {
|
||||
static int fails;
|
||||
|
||||
static void check(boolean ok, String what) {
|
||||
System.out.println((ok ? " ok " : " FAIL ") + what);
|
||||
if (!ok) fails++;
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
final File roots = new File(a[0]);
|
||||
CertVerifier.setRootStore(new CertVerifier.RootStore() {
|
||||
public List<byte[]> find(byte[] subject) {
|
||||
File f = new File(roots, CertVerifier.sha1Hex(subject) + "-0.der");
|
||||
try {
|
||||
return f.exists() ? Collections.singletonList(java.nio.file.Files.readAllBytes(f.toPath())) : null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
NetHttp h = new NetHttp("", 20000);
|
||||
Http.Resp r = h.execHere("GET", "https://nxc.anosanube.ovh/status.php", new String[0], null, null);
|
||||
check(r.code == 200 && r.body.contains("installed"), "Nextcloud over TLS 1.2 with Mozilla roots");
|
||||
try {
|
||||
h.execHere("GET", "https://self-signed.badssl.com/", new String[0], null, null);
|
||||
check(false, "self-signed refused");
|
||||
} catch (DavException.UntrustedCert u) {
|
||||
check(!u.pinMismatch && u.fingerprint.length() == 95, "self-signed -> trust prompt data");
|
||||
Http.Resp p = new NetHttp(u.fingerprint, 20000).execHere("GET", "https://self-signed.badssl.com/", new String[0], null, null);
|
||||
check(p.code == 200, "pinned self-signed accepted");
|
||||
}
|
||||
if (a.length >= 4) {
|
||||
Dav d = new Dav(new NetHttp("", 20000), a[1], a[2], a[3]);
|
||||
String home = d.calendarHome();
|
||||
List<Model.Cal> cals = d.calendars(home);
|
||||
check(home != null && !cals.isEmpty(), "CalDAV discovery: " + cals.size() + " calendars at " + home);
|
||||
}
|
||||
System.out.println(fails == 0 ? "ALL PASSED" : fails + " FAILED");
|
||||
System.exit(fails == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
@@ -57,15 +57,19 @@ public class RedirectTest {
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
// 1. LAN http:// straight at a Nextcloud that forces HTTPS
|
||||
// 1. LAN http:// address, Nextcloud sends its public https:// one: another server, not followed
|
||||
String m = expectError("http://192.168.1.20", new String[][] {{"301", "https://cloud.example.com/remote.php/dav/"}});
|
||||
check(m.startsWith("REDIRECT") && m.contains("forces HTTPS") && m.contains("https://cloud.example.com"), "http->https upgrade explained: " + m.substring(0, 70));
|
||||
// 2. bridge passes Nextcloud's absolute public URL through
|
||||
check(m.startsWith("REDIRECT") && m.contains("Put that address") && m.contains("https://cloud.example.com"), "LAN -> public redirect explained: " + m.substring(0, 70));
|
||||
// 1b. http:// -> https:// on the same host is followed now (the app has its own TLS 1.2)
|
||||
Scripted up = new Scripted(new String[][] {{"301", "https://cloud.example.com/remote.php/dav/"}});
|
||||
new Dav(up, "http://cloud.example.com", "u", "p").multistatus("PROPFIND", "http://cloud.example.com/remote.php/dav/", "0", "<x/>");
|
||||
check(up.urls.size() == 2 && up.urls.get(1).startsWith("https://cloud.example.com/"), "http -> https upgrade followed");
|
||||
// 2. another server's absolute URL
|
||||
m = expectError("https://vps.example.com:8443", new String[][] {{"302", "https://cloud.example.com/remote.php/dav/"}});
|
||||
check(m.startsWith("REDIRECT") && m.contains("proxy_redirect"), "cross-host redirect explained");
|
||||
// 3. bridge with http upstream to a forced-HTTPS Nextcloud: same URL again
|
||||
check(m.startsWith("REDIRECT") && m.contains("Put that address"), "cross-host redirect explained");
|
||||
// 3. the same URL again
|
||||
m = expectError("https://vps.example.com:8443", new String[][] {{"301", "/remote.php/dav/"}, {"301", "/remote.php/dav/"}});
|
||||
check(m.startsWith("REDIRECT") && m.contains("loop") && m.contains("proxy_pass https://"), "loop explained after 1 retry");
|
||||
check(m.startsWith("REDIRECT") && m.contains("loop"), "loop explained after 1 retry");
|
||||
// 4. default port written out is the same server
|
||||
Scripted s = new Scripted(new String[][] {{"301", "https://vps.example.com:443/remote.php/dav/"}});
|
||||
new Dav(s, "https://vps.example.com", "u", "p").multistatus("PROPFIND", "https://vps.example.com/remote.php/dav/", "0", "<x/>");
|
||||
@@ -78,9 +82,9 @@ public class RedirectTest {
|
||||
s = new Scripted(new String[][] {{"301", "https://cloud.example.com/remote.php/dav/"}});
|
||||
try {
|
||||
new Dav(s, "http://192.168.1.20", "u", "p").calendarHome();
|
||||
check(false, "discovery reports forced HTTPS");
|
||||
check(false, "discovery reports the redirect");
|
||||
} catch (DavException e) {
|
||||
check(e.code == DavException.REDIRECT && s.urls.size() == 1, "discovery reports forced HTTPS immediately");
|
||||
check(e.code == DavException.REDIRECT && s.urls.size() == 1, "discovery reports a cross-host redirect immediately");
|
||||
}
|
||||
System.out.println(fails == 0 ? "ALL PASSED" : fails + " FAILED");
|
||||
System.exit(fails == 0 ? 0 : 1);
|
||||
|
||||
Reference in new issue
Block a user