Private
Public Access
Minamp 1.4: faster connecting
- Remember the connection that worked and try it alone first next start - Prefer plain HTTP on the home network (each TLS handshake costs seconds on a 600 MHz phone); new setting "Encrypt at home too" to keep HTTPS - Try secure addresses one at a time instead of in parallel, so they don't all time out together - TLS 1.2 with session resumption: reconnects skip the full handshake - Cache certificate chains already verified; changing the trusted roots clears that cache and all TLS sessions - README: updated connection order; test: session resumption check and HandshakeProfile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58d21b6d68
commit
6fd17bc9c5
9 files changed
+284
-67
No files matched your search
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.minamp"
|
||||
android:versionCode="4"
|
||||
android:versionName="1.3"
|
||||
android:versionCode="5"
|
||||
android:versionName="1.4"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
||||
|
||||
@@ -25,10 +25,15 @@ working one, re-checking whenever the phone switches network:
|
||||
|
||||
| Order | Connection | When it's used |
|
||||
|---|---|---|
|
||||
| 1 | Home network, HTTPS (`*.plex.direct`) | On your Wi-Fi |
|
||||
| 2 | Home network, plain HTTP | Same, if your router blocks plex.direct DNS (rebind protection) |
|
||||
| 3 | Remote, HTTPS | Away, if Remote Access is set up on the server |
|
||||
| 4 | Plex Relay | Away, when the server isn't reachable directly (bandwidth-capped; Minamp caps itself at 192 kbps) |
|
||||
| 1 | Whatever worked last time | Tried alone first, so a normal start costs one request |
|
||||
| 2 | Home network, plain HTTP | On your Wi-Fi (fast: no encryption work for the phone) |
|
||||
| 3 | Home network, HTTPS (`*.plex.direct`) | On your Wi-Fi if plain HTTP is refused, or if *Settings → Encrypt at home too* is on |
|
||||
| 4 | Remote, HTTPS | Away, if Remote Access is set up on the server |
|
||||
| 5 | Plex Relay | Away, when the server isn't reachable directly (bandwidth-capped; Minamp caps itself at 192 kbps) |
|
||||
|
||||
Secure addresses are tried one at a time: on a 600 MHz phone a TLS handshake takes seconds, and
|
||||
several at once make each of them time out. Minamp uses TLS 1.2 with session resumption, so only
|
||||
the first connection to each server pays the full cost; later ones resume in a fraction of it.
|
||||
|
||||
For the best remote experience enable **Settings → Remote Access** on your Plex Media Server.
|
||||
Without it, Relay still works.
|
||||
|
||||
@@ -67,6 +67,19 @@ public class App extends Application {
|
||||
CertVerifier.setRootStore(new AssetRoots(this));
|
||||
Library.preferredVariant = Math.max(0, Math.min(Library.VARIANTS - 1, prefs.getInt("tcVariant", 0)));
|
||||
images = new ImageLoader(this);
|
||||
Server.preferSecureAtHome = prefs.getBoolean("secureHome", false);
|
||||
Server.saver = new Runnable() {
|
||||
public void run() {
|
||||
// remember which connection worked, so next start tries it first
|
||||
final Server s = server;
|
||||
if (s == null) return;
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
if (server == s) prefs.edit().putString("server", s.toJson().toString()).commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
loadServer();
|
||||
PlayQueue.get().restore(this);
|
||||
|
||||
|
||||
@@ -394,6 +394,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
"Quality at home: " + qualityLabel(app.qualityHome()),
|
||||
"Quality away / mobile data: " + qualityLabel(app.qualityAway()),
|
||||
"Download tracks before playing: " + (app.downloadFirst() ? "On" : "Off"),
|
||||
"Encrypt at home too (slower): " + (Server.preferSecureAtHome ? "On" : "Off"),
|
||||
"Library: " + app.sectionTitle(),
|
||||
"Server: " + (s == null ? "-" : s.name),
|
||||
"Connection: " + conn,
|
||||
@@ -411,9 +412,16 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
|
||||
toast("Download before playing: " + (app.downloadFirst() ? "on" : "off"));
|
||||
break;
|
||||
case 3: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
|
||||
case 4: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
|
||||
case 5:
|
||||
case 3:
|
||||
Server.preferSecureAtHome = !Server.preferSecureAtHome;
|
||||
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
|
||||
if (app.server != null) app.server.invalidate();
|
||||
toast(Server.preferSecureAtHome ? "Using HTTPS at home (each new connection is slower on this phone)"
|
||||
: "Using plain HTTP at home (faster)");
|
||||
break;
|
||||
case 4: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
|
||||
case 5: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
|
||||
case 6:
|
||||
if (app.server != null) {
|
||||
app.server.invalidate();
|
||||
toast("Reconnecting…");
|
||||
@@ -425,8 +433,8 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 6: showDiagnostics(); break;
|
||||
case 7: confirmSignOut(); break;
|
||||
case 7: showDiagnostics(); break;
|
||||
case 8: confirmSignOut(); break;
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
|
||||
@@ -45,14 +45,39 @@ public final class CertVerifier {
|
||||
}
|
||||
|
||||
private static RootStore roots;
|
||||
/** Chains already proven to lead to a trusted root (keyed by a hash of the chain), so a
|
||||
* reconnect only re-checks the host name and dates, not every signature again. */
|
||||
private static final java.util.Set<String> verifiedChains = new java.util.HashSet<String>();
|
||||
|
||||
public static void setRootStore(RootStore store) { roots = store; }
|
||||
/** Changing what's trusted invalidates everything trusted so far: cached chains and TLS sessions. */
|
||||
public static void setRootStore(RootStore store) {
|
||||
roots = store;
|
||||
synchronized (verifiedChains) { verifiedChains.clear(); }
|
||||
Http.forgetSessions();
|
||||
}
|
||||
|
||||
private CertVerifier() {}
|
||||
|
||||
|
||||
public static void verify(List<byte[]> chainDer, String host) throws IOException {
|
||||
if (chainDer == null || chainDer.isEmpty()) throw new IOException("Server sent no certificate");
|
||||
if (roots == null) throw new IOException("No trust store configured");
|
||||
java.io.ByteArrayOutputStream all = new java.io.ByteArrayOutputStream();
|
||||
for (int i = 0; i < chainDer.size(); i++) all.write(chainDer.get(i), 0, chainDer.get(i).length);
|
||||
String chainKey = sha1Hex(all.toByteArray());
|
||||
boolean known;
|
||||
synchronized (verifiedChains) { known = verifiedChains.contains(chainKey); }
|
||||
if (known) {
|
||||
Certificate leaf = Certificate.getInstance(chainDer.get(0));
|
||||
if (!hostMatches(leaf, host)) throw new IOException("Certificate does not match " + host);
|
||||
if (new Date().getTime() > 1704067200000L) checkDates(leaf, new Date());
|
||||
return;
|
||||
}
|
||||
verifyFully(chainDer, host);
|
||||
synchronized (verifiedChains) { verifiedChains.add(chainKey); }
|
||||
}
|
||||
|
||||
private static void verifyFully(List<byte[]> chainDer, String host) throws IOException {
|
||||
List<Certificate> chain = new ArrayList<Certificate>();
|
||||
for (int i = 0; i < chainDer.size(); i++) chain.add(Certificate.getInstance(chainDer.get(i)));
|
||||
|
||||
|
||||
@@ -133,6 +133,12 @@ public final class Http {
|
||||
}
|
||||
}
|
||||
|
||||
/** Forget resumable TLS sessions (next connection to each host does a full, verified handshake). */
|
||||
static void forgetSessions() {
|
||||
synchronized (sessions) { sessions.clear(); }
|
||||
evictAll();
|
||||
}
|
||||
|
||||
/** Drop all pooled connections (e.g. after the network changed). */
|
||||
public static void evictAll() {
|
||||
final List<Conn> doomed = new ArrayList<Conn>();
|
||||
@@ -191,6 +197,8 @@ public final class Http {
|
||||
private final String host;
|
||||
private final String sessionKey;
|
||||
|
||||
private final long started = System.currentTimeMillis();
|
||||
|
||||
Client(TlsCrypto crypto, String host, String sessionKey) {
|
||||
super(crypto);
|
||||
this.host = host;
|
||||
@@ -209,6 +217,16 @@ public final class Http {
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS 1.2 only: this TLS stack can resume 1.2 sessions but not 1.3 ones, and on a 600 MHz
|
||||
* phone a resumed handshake (no key exchange, no signature checks) is the difference
|
||||
* between ~10 s and well under a second for every new connection.
|
||||
*/
|
||||
@Override
|
||||
protected com.minamp.bc.tls.ProtocolVersion[] getSupportedVersions() {
|
||||
return com.minamp.bc.tls.ProtocolVersion.TLSv12.only();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TlsSession getSessionToResume() {
|
||||
synchronized (sessions) { return sessions.get(sessionKey); }
|
||||
@@ -217,6 +235,9 @@ public final class Http {
|
||||
@Override
|
||||
public void notifyHandshakeComplete() throws IOException {
|
||||
super.notifyHandshakeComplete();
|
||||
boolean resumed = context.getSecurityParametersConnection().isResumedSession();
|
||||
DiagLog.log("TLS " + (resumed ? "resumed" : "full handshake") + " with " + host
|
||||
+ " in " + (System.currentTimeMillis() - started) + "ms");
|
||||
TlsSession s = context.getSession();
|
||||
if (s != null && s.isResumable()) {
|
||||
synchronized (sessions) { sessions.put(sessionKey, s); }
|
||||
|
||||
+112
-55
@@ -123,68 +123,124 @@ public final class Server {
|
||||
return c.uri;
|
||||
}
|
||||
|
||||
/** When false (default), plain HTTP on the home network is preferred over HTTPS: on a slow
|
||||
* phone every TLS handshake costs seconds, and on the LAN nothing leaves the house. */
|
||||
public static volatile boolean preferSecureAtHome = false;
|
||||
/** Called after a successful pick so the app can save {@link #lastGoodUri}. */
|
||||
public static volatile Runnable saver;
|
||||
|
||||
/** The connection that worked last time; tried first next time. */
|
||||
public volatile String lastGoodUri = "";
|
||||
|
||||
private String probe(Conn c) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
boolean lan = c.kind == LOCAL || c.kind == LOCAL_HTTP || (c.kind == MANUAL && isPrivateAddress(c.uri));
|
||||
Json.Obj o = Plex.fetchJson("GET", c.uri + "/identity", token, lan ? 5000 : 15000);
|
||||
Json.Obj mc = o.optObj("MediaContainer");
|
||||
String id = mc == null ? "" : mc.optString("machineIdentifier");
|
||||
if (machineId.length() > 0 && !machineId.equals(id)) return "a different server answered";
|
||||
com.minamp.net.DiagLog.log("connection ok: " + kindLabel(c.kind) + " " + c.uri + " (" + (System.currentTimeMillis() - t0) + "ms)");
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
String m = com.minamp.net.Threads.describe(e);
|
||||
com.minamp.net.DiagLog.log("connection failed: " + kindLabel(c.kind) + " " + c.uri + ": " + m);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPlainLan(Conn c) {
|
||||
return c.uri.startsWith("http://") && (c.kind == LOCAL_HTTP || (c.kind == MANUAL && isPrivateAddress(c.uri)));
|
||||
}
|
||||
|
||||
private Conn choose(Conn c) {
|
||||
active = c;
|
||||
lastGoodUri = c.uri;
|
||||
Runnable sv = saver;
|
||||
if (sv != null) sv.run();
|
||||
Listener l = listener;
|
||||
if (l != null) l.onConnectionChanged(this);
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Probes every connection in parallel and keeps the most preferred one that answers.
|
||||
* Returns as soon as the best remaining candidate has answered.
|
||||
* Finds a working connection, cheapest first, never running more than one TLS handshake at a
|
||||
* time (parallel handshakes on a single slow core make every one of them time out):
|
||||
* 1. whatever worked last time;
|
||||
* 2. plain-HTTP home-network addresses, in parallel (fast, no TLS);
|
||||
* 3. the rest one by one: home HTTPS, remote, relay.
|
||||
*/
|
||||
public synchronized Conn pick() throws IOException {
|
||||
if (active != null) return active;
|
||||
if (connections.isEmpty()) throw new IOException("No connections for " + name);
|
||||
final int n = connections.size();
|
||||
final int[] state = new int[n]; // 0 pending, 1 ok, 2 failed
|
||||
final String[] errors = new String[n];
|
||||
final Object lock = new Object();
|
||||
for (int i = 0; i < n; i++) {
|
||||
final int idx = i;
|
||||
final Conn c = connections.get(i);
|
||||
Runnable probe = new Runnable() {
|
||||
public void run() {
|
||||
boolean ok = false;
|
||||
try {
|
||||
// Give LAN addresses a short timeout, remote/relay a longer one.
|
||||
int timeout = c.kind <= LOCAL_HTTP ? 3500 : 9000;
|
||||
Json.Obj o = Plex.fetchJson("GET", c.uri + "/identity", token, timeout);
|
||||
Json.Obj mc = o.optObj("MediaContainer");
|
||||
String id = mc == null ? "" : mc.optString("machineIdentifier");
|
||||
ok = machineId.length() == 0 || machineId.equals(id);
|
||||
if (!ok) errors[idx] = "different server answered";
|
||||
} catch (Throwable e) {
|
||||
errors[idx] = com.minamp.net.Threads.describe(e);
|
||||
}
|
||||
synchronized (lock) {
|
||||
state[idx] = ok ? 1 : 2;
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
com.minamp.net.Threads.start(probe, "probe-" + i);
|
||||
}
|
||||
long deadline = System.currentTimeMillis() + 12000;
|
||||
synchronized (lock) {
|
||||
while (true) {
|
||||
// the first non-failed candidate in preference order decides
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (state[i] == 2) continue;
|
||||
if (state[i] == 1) {
|
||||
active = connections.get(i);
|
||||
Listener l = listener;
|
||||
if (l != null) l.onConnectionChanged(this);
|
||||
return active;
|
||||
}
|
||||
break; // still waiting on a better candidate
|
||||
}
|
||||
boolean allFailed = true;
|
||||
for (int i = 0; i < n; i++) if (state[i] != 2) allFailed = false;
|
||||
long left = deadline - System.currentTimeMillis();
|
||||
if (allFailed || left <= 0) break;
|
||||
try { lock.wait(left); } catch (InterruptedException e) { break; }
|
||||
List<Conn> order = new ArrayList<Conn>(connections);
|
||||
if (preferSecureAtHome) {
|
||||
// home HTTPS ahead of home HTTP
|
||||
for (int i = 0; i < order.size(); i++) {
|
||||
if (order.get(i).kind == LOCAL) { order.add(0, order.remove(i)); }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("Can't reach " + (name.length() > 0 ? name : "server"));
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (errors[i] != null) { sb.append("\n").append(kindLabel(connections.get(i).kind)).append(": ").append(errors[i]); break; }
|
||||
String firstError = null;
|
||||
List<Conn> tried = new ArrayList<Conn>();
|
||||
|
||||
// 1. last known good
|
||||
for (int i = 0; i < order.size(); i++) {
|
||||
Conn c = order.get(i);
|
||||
if (c.uri.equals(lastGoodUri)) {
|
||||
tried.add(c);
|
||||
String err = probe(c);
|
||||
if (err == null) return choose(c);
|
||||
firstError = kindLabel(c.kind) + ": " + err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new IOException(sb.toString());
|
||||
|
||||
// 2. plain LAN addresses together
|
||||
final List<Conn> plain = new ArrayList<Conn>();
|
||||
if (!preferSecureAtHome) {
|
||||
for (int i = 0; i < order.size(); i++) if (isPlainLan(order.get(i)) && !tried.contains(order.get(i))) plain.add(order.get(i));
|
||||
}
|
||||
if (!plain.isEmpty()) {
|
||||
final String[] errs = new String[plain.size()];
|
||||
final int[] state = new int[plain.size()];
|
||||
final Object lock = new Object();
|
||||
for (int i = 0; i < plain.size(); i++) {
|
||||
final int idx = i;
|
||||
com.minamp.net.Threads.start(new Runnable() {
|
||||
public void run() {
|
||||
String e = probe(plain.get(idx));
|
||||
synchronized (lock) { errs[idx] = e; state[idx] = e == null ? 1 : 2; lock.notifyAll(); }
|
||||
}
|
||||
}, "probe-" + i);
|
||||
}
|
||||
long deadline = System.currentTimeMillis() + 7000;
|
||||
synchronized (lock) {
|
||||
while (true) {
|
||||
boolean pending = false;
|
||||
for (int i = 0; i < plain.size(); i++) {
|
||||
if (state[i] == 1) return choose(plain.get(i));
|
||||
if (state[i] == 0) pending = true;
|
||||
}
|
||||
long left = deadline - System.currentTimeMillis();
|
||||
if (!pending || left <= 0) break;
|
||||
try { lock.wait(left); } catch (InterruptedException e) { break; }
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < plain.size(); i++) {
|
||||
tried.add(plain.get(i));
|
||||
if (firstError == null && errs[i] != null) firstError = kindLabel(plain.get(i).kind) + ": " + errs[i];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. everything else, one at a time, in preference order
|
||||
for (int i = 0; i < order.size(); i++) {
|
||||
Conn c = order.get(i);
|
||||
if (tried.contains(c)) continue;
|
||||
String err = probe(c);
|
||||
if (err == null) return choose(c);
|
||||
if (firstError == null) firstError = kindLabel(c.kind) + ": " + err;
|
||||
}
|
||||
throw new IOException("Can't reach " + (name.length() > 0 ? name : "server") + (firstError != null ? "\n" + firstError : ""));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ requests
|
||||
@@ -266,7 +322,7 @@ public final class Server {
|
||||
|
||||
public Json.Obj toJson() {
|
||||
Json.Obj o = new Json.Obj();
|
||||
o.put("name", name).put("id", machineId).put("token", token).put("owned", Boolean.valueOf(owned));
|
||||
o.put("name", name).put("id", machineId).put("token", token).put("owned", Boolean.valueOf(owned)).put("last", lastGoodUri);
|
||||
Json.Arr a = new Json.Arr();
|
||||
for (int i = 0; i < connections.size(); i++) {
|
||||
Conn c = connections.get(i);
|
||||
@@ -282,6 +338,7 @@ public final class Server {
|
||||
s.machineId = o.optString("id");
|
||||
s.token = o.optString("token");
|
||||
s.owned = o.optBool("owned");
|
||||
s.lastGoodUri = o.optString("last", "");
|
||||
Json.Arr a = o.optArr("conns");
|
||||
if (a != null) {
|
||||
for (int i = 0; i < a.size(); i++) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import com.minamp.net.*;
|
||||
|
||||
/** Samples the handshake thread's stack to see where TLS time goes (run with -Xint). */
|
||||
public class HandshakeProfile {
|
||||
public static void main(String[] a) throws Exception {
|
||||
CertVerifier.setRootStore(new PlexTest.RootDir(new File(a[0])));
|
||||
final String url = a[1];
|
||||
final int rounds = Integer.parseInt(a[2]);
|
||||
final Map<String, Integer> buckets = new TreeMap<String, Integer>();
|
||||
final long[] times = new long[rounds];
|
||||
final Thread[] worker = new Thread[1];
|
||||
worker[0] = Threads.create(new Runnable() { public void run() {
|
||||
for (int i = 0; i < rounds; i++) {
|
||||
Http.evictAll();
|
||||
long t0 = System.nanoTime();
|
||||
try { Http.get(url, null, 60000).readAll(); } catch (Exception e) { System.out.println(e); }
|
||||
times[i] = (System.nanoTime() - t0) / 1000000;
|
||||
}
|
||||
}}, "hs");
|
||||
worker[0].start();
|
||||
while (worker[0].isAlive()) {
|
||||
StackTraceElement[] st = worker[0].getStackTrace();
|
||||
String b = "other";
|
||||
for (StackTraceElement e : st) {
|
||||
String c = e.getClassName() + "." + e.getMethodName();
|
||||
if (c.startsWith("com.minamp.net.CertVerifier.signatureValid")) { b = "our chain check: signature math"; break; }
|
||||
if (c.startsWith("com.minamp.net.CertVerifier")) { b = "our chain check: other"; break; }
|
||||
if (c.contains("Ed25519") || c.contains("X25519") || c.contains("rfc7748")) { b = "X25519 key exchange"; break; }
|
||||
if (c.contains("ECDSASigner") || c.contains("DSADigestSigner")) { b = "ECDSA verify (server signature)"; break; }
|
||||
if (c.contains("RSA")) { b = "RSA"; break; }
|
||||
if (c.contains("math.ec")) { b = "EC math (other)"; break; }
|
||||
if (c.contains("asn1")) { b = "ASN.1 parsing"; break; }
|
||||
if (c.contains("ChaCha") || c.contains("Poly1305") || c.contains("AES") || c.contains("GCM")) { b = "symmetric cipher"; break; }
|
||||
if (c.contains("Digest") || c.contains("HMAC") || c.contains("HKDF")) { b = "hashing/HKDF"; break; }
|
||||
if (c.contains("SecureRandom") || c.contains("Random")) { b = "random"; break; }
|
||||
if (c.contains("java.net") || c.contains("SocketInputStream") || c.contains("NioSocket") || c.contains("read0")) { b = "network wait"; break; }
|
||||
}
|
||||
Integer n = buckets.get(b); buckets.put(b, n == null ? 1 : n + 1);
|
||||
Thread.sleep(3);
|
||||
}
|
||||
int total = 0; for (int v : buckets.values()) total += v;
|
||||
System.out.println("handshake ms: " + Arrays.toString(times));
|
||||
List<Map.Entry<String, Integer>> l = new ArrayList<Map.Entry<String, Integer>>(buckets.entrySet());
|
||||
Collections.sort(l, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> x, Map.Entry<String, Integer> y) { return y.getValue() - x.getValue(); } });
|
||||
for (Map.Entry<String, Integer> e : l) System.out.printf(" %5.1f%% %s%n", 100.0 * e.getValue() / total, e.getKey());
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,45 @@ public class PlexTest {
|
||||
Server.Conn c3 = s3.pick();
|
||||
check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri);
|
||||
|
||||
// --- TLS 1.2 session resumption: the second connection must skip the full handshake
|
||||
com.minamp.net.DiagLog.clear();
|
||||
Http.evictAll();
|
||||
Thread.sleep(300);
|
||||
Http.get("https://127-0-0-1.abc123.plex.direct:32443/identity", null, 10000).readAll();
|
||||
Http.evictAll();
|
||||
Thread.sleep(300);
|
||||
long tr = System.currentTimeMillis();
|
||||
Http.get("https://127-0-0-1.abc123.plex.direct:32443/identity", null, 10000).readAll();
|
||||
tr = System.currentTimeMillis() - tr;
|
||||
String dl = com.minamp.net.DiagLog.dump();
|
||||
check(dl.indexOf("TLS resumed") >= 0, "second TLS connection resumed the session (" + tr + "ms)");
|
||||
|
||||
// --- picking: last good first, and a dead preferred address doesn't block a working one
|
||||
Server s5 = new Server();
|
||||
s5.machineId = "machine-abc123"; s5.token = s.token; s5.name = "x";
|
||||
s5.add("https://10-255-255-1.abc123.plex.direct:9", Server.LOCAL); // unreachable
|
||||
s5.add("http://127.0.0.1:32400", Server.LOCAL_HTTP);
|
||||
s5.add("https://127-0-0-1.abc123.plex.direct:32443", Server.REMOTE);
|
||||
s5.sortConnections();
|
||||
long tp = System.currentTimeMillis();
|
||||
Server.Conn c5 = s5.pick();
|
||||
tp = System.currentTimeMillis() - tp;
|
||||
check(c5.kind == Server.LOCAL_HTTP && tp < 3000, "plain home address chosen straight away (" + tp + "ms), not stuck behind HTTPS");
|
||||
Server s6 = Server.fromJson(Json.parseObj(s5.toJson().toString()));
|
||||
check(s6.lastGoodUri.equals("http://127.0.0.1:32400"), "working address remembered across restarts");
|
||||
s6.lastGoodUri = "https://127-0-0-1.abc123.plex.direct:32443";
|
||||
com.minamp.net.DiagLog.clear();
|
||||
Server.Conn c6 = s6.pick();
|
||||
check(c6.kind == Server.REMOTE && com.minamp.net.DiagLog.dump().indexOf("127.0.0.1:32400") < 0, "last good connection tried first, nothing else probed");
|
||||
Server s7 = new Server();
|
||||
s7.machineId = "machine-abc123"; s7.token = s.token; s7.name = "x";
|
||||
s7.add("http://10.255.255.1:32400", Server.LOCAL_HTTP); // away from home: LAN dead
|
||||
s7.add("https://127-0-0-1.abc123.plex.direct:32443", Server.REMOTE);
|
||||
s7.add("https://127-0-0-1.abc123.plex.direct:32443/relay", Server.RELAY);
|
||||
s7.sortConnections();
|
||||
Server.Conn c7 = s7.pick();
|
||||
check(c7.kind == Server.REMOTE, "away from home: falls through to remote (" + Server.kindLabel(c7.kind) + ")");
|
||||
|
||||
// --- manual LAN address without a token (server allows this network without sign-in)
|
||||
Server lan = new Server();
|
||||
lan.name = "manual"; lan.token = "";
|
||||
|
||||
Reference in new issue
Block a user