MiniABS 1.1: HTTPS support

- Bundle current root certificates (Let's Encrypt, GTS, Sectigo, DigiCert G2,
  Amazon, ...) missing from Android 2.x's store (Roots.java, tools/gen_roots.py)
- Ask once for unknown certificates, showing the SHA-256 fingerprint, and
  remember the answer; replaces "accept any certificate" (Trust*, Settings)
- Local streaming proxy so Android 2.x's MediaPlayer can play https:// servers,
  with seeking and a fresh token per request (StreamProxy.java)
- Tests over real TLS with good, expired, wrong-host, forged and self-signed
  certificates (test/make_pki.sh, TrustTest.java)
- README: server setup for a legacy TLS 1.0 port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 20:07:55 +02:00
1 parent 4fe9eed870
commit b7e43d7d61
24 files changed
+1326 -88

No files matched your search

+9 -50
View File
@@ -12,17 +12,10 @@ import java.net.ProtocolException;
import java.net.Socket;
import java.net.URL;
import java.net.URLEncoder;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.json.JSONArray;
import org.json.JSONException;
@@ -33,7 +26,7 @@ import android.os.Build;
/** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */
final class Api {
static final String VERSION = "1.0";
static final String VERSION = "1.1";
static class ApiException extends IOException {
final int code;
@@ -52,7 +45,6 @@ final class Api {
}
private static final Object REFRESH_LOCK = new Object();
private static SSLSocketFactory trustAllFactory;
static {
// Froyo's HttpURLConnection reuses dead pooled sockets; turn pooling off.
@@ -70,7 +62,7 @@ final class Api {
conn.setReadTimeout(30000);
conn.setUseCaches(false);
conn.setInstanceFollowRedirects(true);
if (conn instanceof HttpsURLConnection && Prefs.get(c).trustAll()) trustAll((HttpsURLConnection) conn);
if (conn instanceof HttpsURLConnection) Trust.apply(c, (HttpsURLConnection) conn);
setMethod(conn, method);
conn.setRequestProperty("User-Agent", "MiniABS/" + VERSION + " (Android " + Build.VERSION.RELEASE + ")");
return conn;
@@ -80,38 +72,6 @@ final class Api {
conn.setRequestMethod(method);
}
private static synchronized SSLSocketFactory trustAllFactory() throws Exception {
if (trustAllFactory == null) {
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String type) {
}
public void checkServerTrusted(X509Certificate[] chain, String type) {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{tm}, null);
trustAllFactory = ctx.getSocketFactory();
}
return trustAllFactory;
}
private static void trustAll(HttpsURLConnection conn) {
try {
conn.setSSLSocketFactory(trustAllFactory());
conn.setHostnameVerifier(new HostnameVerifier() {
public boolean verify(String host, SSLSession session) {
return true;
}
});
} catch (Exception ignored) {
}
}
/**
* PATCH over a plain socket. Android 2.x's HttpURLConnection only sends a body with POST or PUT,
* and Audiobookshelf uses PATCH for "mark as played" and for saving progress without a session.
@@ -130,16 +90,15 @@ final class Api {
sock.setSoTimeout(30000);
Socket io = sock;
if (https) {
boolean trust = p.trustAll();
SSLSocketFactory f;
try {
f = trust ? trustAllFactory() : (SSLSocketFactory) SSLSocketFactory.getDefault();
} catch (Exception e) {
throw new SSLException("TLS unavailable");
SSLSocket ss = (SSLSocket) Trust.factory(c).createSocket(sock, u.getHost(), port, true);
try { // send the host name (SNI) where the phone supports it (Android 2.3+)
ss.getClass().getMethod("setHostname", String.class).invoke(ss, u.getHost());
} catch (Exception ignored) {
}
SSLSocket ss = (SSLSocket) f.createSocket(sock, u.getHost(), port, true);
ss.startHandshake();
if (!trust && !HttpsURLConnection.getDefaultHostnameVerifier().verify(u.getHost(), ss.getSession())) {
java.security.cert.Certificate[] peer = ss.getSession().getPeerCertificates();
if (peer.length == 0 || !(peer[0] instanceof java.security.cert.X509Certificate)
|| !Trust.hostMatches((java.security.cert.X509Certificate) peer[0], u.getHost())) {
ss.close();
throw new SSLException("Certificate doesn't match " + u.getHost());
}