Compare commits
4
Commits
66e6b79163
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdf8de2767 | ||
|
|
3fa210fc60 | ||
|
|
e1dad69c5b | ||
|
|
c5c3973878 |
No files matched your search
+18
-6
@@ -4,35 +4,47 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
/** Mozilla roots shipped as assets/roots/<sha1(subject)>-<n>.der */
|
/**
|
||||||
|
* Mozilla roots shipped as assets/roots/<sha1(subject)>-<n>.der
|
||||||
|
*
|
||||||
|
* Roots are read from the APK once and kept: an app installed on the SD card loses its APK while the
|
||||||
|
* card is shared over USB, and a failed read must not leave every server "untrusted" until a restart.
|
||||||
|
*/
|
||||||
public final class AssetRoots implements CertVerifier.RootStore {
|
public final class AssetRoots implements CertVerifier.RootStore {
|
||||||
private final Context ctx;
|
private final Context ctx;
|
||||||
private Set<String> names;
|
private Set<String> names; // asset file names, once listed
|
||||||
|
private final Map<String, List<byte[]>> found = new HashMap<String, List<byte[]>>(); // by subject hash; null = none
|
||||||
|
|
||||||
public AssetRoots(Context c) { ctx = c.getApplicationContext(); }
|
public AssetRoots(Context c) { ctx = c.getApplicationContext(); }
|
||||||
|
|
||||||
public synchronized List<byte[]> find(byte[] subjectDer) {
|
public synchronized List<byte[]> find(byte[] subjectDer) {
|
||||||
|
String h = CertVerifier.sha1Hex(subjectDer);
|
||||||
|
if (found.containsKey(h)) return found.get(h);
|
||||||
try {
|
try {
|
||||||
if (names == null) {
|
if (names == null) {
|
||||||
names = new HashSet<String>();
|
|
||||||
String[] l = ctx.getAssets().list("roots");
|
String[] l = ctx.getAssets().list("roots");
|
||||||
if (l != null) for (int i = 0; i < l.length; i++) names.add(l[i]);
|
if (l == null || l.length == 0) return null; // APK unreadable right now: try again next time
|
||||||
|
Set<String> n = new HashSet<String>();
|
||||||
|
for (int i = 0; i < l.length; i++) n.add(l[i]);
|
||||||
|
names = n;
|
||||||
}
|
}
|
||||||
String h = CertVerifier.sha1Hex(subjectDer);
|
|
||||||
List<byte[]> out = null;
|
List<byte[]> out = null;
|
||||||
for (int i = 0; names.contains(h + "-" + i + ".der"); i++) {
|
for (int i = 0; names.contains(h + "-" + i + ".der"); i++) {
|
||||||
if (out == null) out = new ArrayList<byte[]>();
|
if (out == null) out = new ArrayList<byte[]>();
|
||||||
out.add(readAll(ctx.getAssets().open("roots/" + h + "-" + i + ".der")));
|
out.add(readAll(ctx.getAssets().open("roots/" + h + "-" + i + ".der")));
|
||||||
}
|
}
|
||||||
|
found.put(h, out);
|
||||||
return out;
|
return out;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return null;
|
return null; // not cached: the next handshake reads again
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-16
@@ -451,19 +451,25 @@ public final class Http {
|
|||||||
// ------------------------------------------------------------------ body streams
|
// ------------------------------------------------------------------ body streams
|
||||||
|
|
||||||
private static final class FixedStream extends InputStream {
|
private static final class FixedStream extends InputStream {
|
||||||
private final Conn c; private long left; private final boolean keepAlive; private boolean closed;
|
private final Conn c; private long left; private final boolean keepAlive; private volatile boolean closed;
|
||||||
|
|
||||||
FixedStream(Conn c, long len, boolean keepAlive) {
|
FixedStream(Conn c, long len, boolean keepAlive) {
|
||||||
this.c = c; this.left = len; this.keepAlive = keepAlive;
|
this.c = c; this.left = len; this.keepAlive = keepAlive;
|
||||||
if (len == 0) finish(true);
|
if (len == 0) finish(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finish(boolean complete) {
|
private synchronized void finish(boolean complete) {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
closed = true;
|
closed = true;
|
||||||
if (complete && keepAlive) release(c); else c.close();
|
if (complete && keepAlive) release(c); else c.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void abort() {
|
||||||
|
if (closed) return; // done: the connection may already be back in the pool, serving someone else
|
||||||
|
closed = true;
|
||||||
|
try { c.socket.close(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
@Override public int read() throws IOException {
|
@Override public int read() throws IOException {
|
||||||
byte[] one = new byte[1];
|
byte[] one = new byte[1];
|
||||||
int n = read(one, 0, 1);
|
int n = read(one, 0, 1);
|
||||||
@@ -490,16 +496,22 @@ public final class Http {
|
|||||||
|
|
||||||
private static final class ChunkedStream extends InputStream {
|
private static final class ChunkedStream extends InputStream {
|
||||||
private final Conn c; private final boolean keepAlive;
|
private final Conn c; private final boolean keepAlive;
|
||||||
private long chunkLeft = 0; private boolean eof; private boolean closed;
|
private long chunkLeft = 0; private boolean eof; private volatile boolean closed;
|
||||||
|
|
||||||
ChunkedStream(Conn c, boolean keepAlive) { this.c = c; this.keepAlive = keepAlive; }
|
ChunkedStream(Conn c, boolean keepAlive) { this.c = c; this.keepAlive = keepAlive; }
|
||||||
|
|
||||||
private void finish(boolean complete) {
|
private synchronized void finish(boolean complete) {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
closed = true;
|
closed = true;
|
||||||
if (complete && keepAlive) release(c); else c.close();
|
if (complete && keepAlive) release(c); else c.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void abort() {
|
||||||
|
if (closed) return; // done: the connection may already be back in the pool, serving someone else
|
||||||
|
closed = true;
|
||||||
|
try { c.socket.close(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean nextChunk() throws IOException {
|
private boolean nextChunk() throws IOException {
|
||||||
if (eof) return false;
|
if (eof) return false;
|
||||||
String line = readLine(c.in);
|
String line = readLine(c.in);
|
||||||
@@ -551,7 +563,7 @@ public final class Http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class EofStream extends InputStream {
|
private static final class EofStream extends InputStream {
|
||||||
private final Conn c; private boolean closed;
|
private final Conn c; private volatile boolean closed;
|
||||||
EofStream(Conn c) { this.c = c; }
|
EofStream(Conn c) { this.c = c; }
|
||||||
|
|
||||||
@Override public int read() throws IOException {
|
@Override public int read() throws IOException {
|
||||||
@@ -568,21 +580,25 @@ public final class Http {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void close() {
|
@Override public synchronized void close() {
|
||||||
if (!closed) { closed = true; c.close(); }
|
if (!closed) { closed = true; c.close(); }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** Close one in-flight response's socket from another thread (used to abort streams). */
|
synchronized void abort() {
|
||||||
public static void abort(Response r) {
|
if (closed) return;
|
||||||
if (r == null) return;
|
closed = true;
|
||||||
InputStream b = r.body;
|
|
||||||
Conn c = null;
|
|
||||||
if (b instanceof FixedStream) c = ((FixedStream) b).c;
|
|
||||||
else if (b instanceof ChunkedStream) c = ((ChunkedStream) b).c;
|
|
||||||
else if (b instanceof EofStream) c = ((EofStream) b).c;
|
|
||||||
if (c != null) {
|
|
||||||
try { c.socket.close(); } catch (Exception ignored) {}
|
try { c.socket.close(); } catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Close one in-flight response's socket from another thread (used to abort streams). A response that has
|
||||||
|
* already been read to the end is left alone: its connection went back to the pool and may be carrying
|
||||||
|
* another request by now (closing it cut read-ahead downloads short: "Unexpected end of stream"). */
|
||||||
|
public static void abort(Response r) {
|
||||||
|
if (r == null) return;
|
||||||
|
InputStream b = r.body;
|
||||||
|
if (b instanceof FixedStream) ((FixedStream) b).abort();
|
||||||
|
else if (b instanceof ChunkedStream) ((ChunkedStream) b).abort();
|
||||||
|
else if (b instanceof EofStream) ((EofStream) b).abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# tlskit — spec
|
||||||
|
|
||||||
|
The short contract for working on the shared HTTP/TLS client. `README.md` explains its use; this file says
|
||||||
|
what it promises to the apps that copy it.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Let Android 2.2/2.3 apps reach modern `https://` servers (TLS 1.2, today's roots, SNI), which the
|
||||||
|
system's own TLS 1.0 stack can't.
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
Copied into: Plexamp Lite, Podlite, Noticias Lite, Calendar Lite (by hand, under
|
||||||
|
`android/org/gingercal/net`), AcasaLite, Tenda Lite and WikipeLite. The apps carry copies, not a dependency.
|
||||||
|
|
||||||
|
## What it provides
|
||||||
|
|
||||||
|
- `net/Http.java`: HTTP/1.1 on Bouncy Castle TLS 1.2 (renamed `com.minamp.bc`), keep-alive pooling, session
|
||||||
|
resumption, chunked bodies.
|
||||||
|
- `net/CertVerifier.java`: host name, validity dates, chain up to a bundled Mozilla root (trust anchors must
|
||||||
|
be CAs); optionally one pinned leaf certificate (SHA-256), with the fingerprint shown to the user. Untrusted
|
||||||
|
errors carry the chain.
|
||||||
|
- `net/AssetRoots.java` + `roots/`: Mozilla's roots as `assets/roots/<sha1(subject)>-<n>.der`; loaded roots
|
||||||
|
are kept in memory and a failed read is retried.
|
||||||
|
- `net/Threads.java`: threads with a 256 KB stack; network work must run on one.
|
||||||
|
- `net/DiagLog.java`: connection log with tokens removed from URLs.
|
||||||
|
|
||||||
|
## Must not break
|
||||||
|
|
||||||
|
- Must run on Dalvik for API 8: no newer Java or Android APIs.
|
||||||
|
- Must not clash with the old Bouncy Castle inside Android (keep the `com.minamp.bc` rename).
|
||||||
|
- Never log tokens or passwords.
|
||||||
|
- A change here is not done until it has been reinstalled into every app above and each one rebuilt
|
||||||
|
(and released through Tenda).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
- `./install.sh <app dir> <java package>` copies `net/` into the app's package and `roots/` into its `assets/`.
|
||||||
|
- Bouncy Castle sources come from Plexamp Lite's `tools/setup_toolchain.sh` (`~/android-toolchain/bcsrc`);
|
||||||
|
roots are regenerated with Plexamp Lite's `tools/make_roots.py`.
|
||||||
|
- Test: `test/KitTest.java` on a desktop JVM against real FreshRSS, Nextcloud and Audiobookshelf servers.
|
||||||
Reference in new issue
Block a user