MiniABS 1.7: bundled TLS 1.2, no legacy HTTPS port needed
All requests (API, covers, downloads, the streaming proxy) go through Minamp's own HTTP/TLS client (Bouncy Castle TLS 1.2, Mozilla roots, keep-alive, session resumption) via NetConnection, a drop-in HttpURLConnection, as the iPod version does with BearSSL. The phone reaches the normal https:// address directly; nothing changes on the server. - Trust: Mozilla roots + the user's pinned CAs/certificates feed the new verifier; the trust prompt, renew-safe CA pinning and forged-chain protection behave as before - PATCH is a normal request now (no raw-socket path) - Job, covers, player, downloads and proxy threads get big stacks for the handshake - embedded Roots.java and gen_roots.py replaced by assets/roots - tests: desktop shims read assets; all 104 checks pass (HTTP and HTTPS) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6cbe3596e4
commit
06a5c7078f
145 files changed
+1400
-549
No files matched your search
+19
-59
@@ -26,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.6";
|
||||
static final String VERSION = "1.7";
|
||||
|
||||
static class ApiException extends IOException {
|
||||
final int code;
|
||||
@@ -57,12 +57,12 @@ final class Api {
|
||||
// ------------------------------------------------------------------ plumbing
|
||||
|
||||
static HttpURLConnection open(Context c, String url, String method) throws IOException {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
Trust.install(c);
|
||||
HttpURLConnection conn = new NetConnection(new URL(url));
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(30000);
|
||||
conn.setUseCaches(false);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
if (conn instanceof HttpsURLConnection) Trust.apply(c, (HttpsURLConnection) conn);
|
||||
setMethod(conn, method);
|
||||
conn.setRequestProperty("User-Agent", "MiniABS/" + VERSION + " (Android " + Build.VERSION.RELEASE + ")");
|
||||
return conn;
|
||||
@@ -73,71 +73,31 @@ final class Api {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* PATCH: Audiobookshelf uses it for "mark as played" and for saving progress without a session.
|
||||
* (Android 2.x's own HttpURLConnection can't send it; the bundled client can.)
|
||||
*/
|
||||
private static Object rawPatch(Context c, String path, JSONObject body, boolean mayRefresh) throws IOException {
|
||||
Prefs p = Prefs.get(c);
|
||||
if (mayRefresh) ensureFresh(c, 120);
|
||||
String used = p.token();
|
||||
URL u = new URL(p.server() + path);
|
||||
boolean https = "https".equalsIgnoreCase(u.getProtocol());
|
||||
int port = u.getPort() > 0 ? u.getPort() : (https ? 443 : 80);
|
||||
Socket sock = new Socket();
|
||||
HttpURLConnection conn = open(c, p.server() + path, "PATCH");
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
conn.setRequestProperty("Authorization", "Bearer " + used);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
int code;
|
||||
try {
|
||||
sock.connect(new InetSocketAddress(u.getHost(), port), 15000);
|
||||
sock.setSoTimeout(30000);
|
||||
Socket io = sock;
|
||||
if (https) {
|
||||
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) {
|
||||
}
|
||||
ss.startHandshake();
|
||||
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("O certificado non corresponde a " + u.getHost());
|
||||
}
|
||||
io = ss;
|
||||
OutputStream os = conn.getOutputStream();
|
||||
os.write((body == null ? "{}" : body.toString()).getBytes("UTF-8"));
|
||||
code = conn.getResponseCode();
|
||||
InputStream in = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
||||
if (in != null) {
|
||||
byte[] rest = new byte[1024];
|
||||
while (in.read(rest) > 0) { } // read the reply so the connection can be reused
|
||||
}
|
||||
byte[] b = (body == null ? "{}" : body.toString()).getBytes("UTF-8");
|
||||
String host = u.getHost() + (u.getPort() > 0 ? ":" + u.getPort() : "");
|
||||
String head = "PATCH " + u.getFile() + " HTTP/1.1\r\n"
|
||||
+ "Host: " + host + "\r\n"
|
||||
+ "Authorization: Bearer " + used + "\r\n"
|
||||
+ "Content-Type: application/json\r\n"
|
||||
+ "Accept: application/json\r\n"
|
||||
+ "User-Agent: MiniABS/" + VERSION + "\r\n"
|
||||
+ "Content-Length: " + b.length + "\r\n"
|
||||
+ "Connection: close\r\n\r\n";
|
||||
OutputStream os = io.getOutputStream();
|
||||
os.write(head.getBytes("UTF-8"));
|
||||
os.write(b);
|
||||
os.flush();
|
||||
InputStream in = io.getInputStream();
|
||||
StringBuilder line = new StringBuilder();
|
||||
int ch;
|
||||
while ((ch = in.read()) != -1 && ch != '\n' && line.length() < 200) line.append((char) ch);
|
||||
String[] parts = line.toString().trim().split(" ");
|
||||
if (parts.length < 2 || !parts[0].startsWith("HTTP/")) throw new IOException("Resposta inesperada do servidor");
|
||||
try {
|
||||
code = Integer.parseInt(parts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Resposta inesperada do servidor");
|
||||
}
|
||||
byte[] rest = new byte[1024];
|
||||
int total = 0, n;
|
||||
while (total < 16384 && (n = in.read(rest)) > 0) total += n; // read the reply politely
|
||||
io.close();
|
||||
} finally {
|
||||
try {
|
||||
sock.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
conn.disconnect();
|
||||
}
|
||||
if (code == 401) {
|
||||
if (mayRefresh && p.canRefresh()) {
|
||||
|
||||
Reference in new issue
Block a user