Private
Public Access
Minamp 1.3
- Fix NoSuchMethodError on Android 2.x: Dalvik before 4.0 can't link Object methods (toString/equals/...) called through an interface. Patch the bundled Bouncy Castle (tools/patch_bc_dalvik.py) and our own CertVerifier, and make build.sh fail if tools/scan_dalvik.py finds any such call - Treat a manually entered private address (10.x, 172.16-31.x, 192.168.x, .local) as home, so LAN servers that allow access without sign-in work - Send Plex client headers on media requests (files and transcodes) - Explain a 503 on a file as the server being unable to open it (drive or folder not available) - Connection log: home/away, token presence, Plex's file check and a direct file request in the streaming test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4b875ef34
commit
58d21b6d68
14 files changed
+218
-13
No files matched your search
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.minamp"
|
package="com.minamp"
|
||||||
android:versionCode="3"
|
android:versionCode="4"
|
||||||
android:versionName="1.2"
|
android:versionName="1.3"
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ javac -nowarn -Xlint:-options -source 8 -target 8 -encoding UTF-8 -g:source,line
|
|||||||
-d $OUT/classes @$OUT/sources.txt 2>&1 | grep -v -E '^Picked up|^Note:' || true
|
-d $OUT/classes @$OUT/sources.txt 2>&1 | grep -v -E '^Picked up|^Note:' || true
|
||||||
test -f $OUT/classes/com/minamp/HomeActivity.class
|
test -f $OUT/classes/com/minamp/HomeActivity.class
|
||||||
|
|
||||||
|
echo "== Dalvik 2.x link check"
|
||||||
|
python3 tools/scan_dalvik.py $OUT/classes $ANDROID_JAR > $OUT/dalvik-scan.txt
|
||||||
|
if ! grep -q "^total patterns: 0$" $OUT/dalvik-scan.txt; then
|
||||||
|
cat $OUT/dalvik-scan.txt; echo "Calls Android 2.x can't link (see above): fix before shipping"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "== dex"
|
echo "== dex"
|
||||||
java -Xmx2g -jar $T/dx.jar --dex --output=$OUT/classes.dex $OUT/classes 2>&1 | grep -v '^Picked up' || true
|
java -Xmx2g -jar $T/dx.jar --dex --output=$OUT/classes.dex $OUT/classes 2>&1 | grep -v '^Picked up' || true
|
||||||
|
|
||||||
|
|||||||
@@ -499,7 +499,22 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
|||||||
}
|
}
|
||||||
lib.ensureMedia(t);
|
lib.ensureMedia(t);
|
||||||
com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via "
|
com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via "
|
||||||
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind)));
|
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind))
|
||||||
|
+ (lib.server.isLocal() ? " (home)" : " (away)"));
|
||||||
|
String tok = lib.server.token;
|
||||||
|
com.minamp.net.DiagLog.log("token: " + (tok == null || tok.length() == 0 ? "none (relying on the server allowing this network without sign-in)" : "set, " + tok.length() + " chars"));
|
||||||
|
com.minamp.net.DiagLog.log("server file check: " + lib.checkPart(t));
|
||||||
|
com.minamp.net.Http.Response fr = null;
|
||||||
|
try {
|
||||||
|
java.util.Map<String, String> h = lib.server.mediaHeaders();
|
||||||
|
h.put("Range", "bytes=0-15");
|
||||||
|
fr = com.minamp.net.Http.get(lib.directUrl(t), h, 20000);
|
||||||
|
com.minamp.net.DiagLog.log("original file request: " + fr.code + " " + (fr.code >= 400 ? com.minamp.plex.Plex.snippet(fr.readString()) : fr.header("content-type")));
|
||||||
|
} catch (Exception e) {
|
||||||
|
com.minamp.net.DiagLog.log("original file request failed: " + com.minamp.net.Threads.describe(e));
|
||||||
|
} finally {
|
||||||
|
com.minamp.net.Http.abort(fr);
|
||||||
|
}
|
||||||
int kbps = app.currentMaxKbps();
|
int kbps = app.currentMaxKbps();
|
||||||
if (kbps <= 0) kbps = 320;
|
if (kbps <= 0) kbps = 320;
|
||||||
for (int v = 0; v < Library.VARIANTS; v++) {
|
for (int v = 0; v < Library.VARIANTS; v++) {
|
||||||
@@ -508,7 +523,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
|||||||
String url = lib.transcodeUrl(t, kbps, 0, session, v);
|
String url = lib.transcodeUrl(t, kbps, 0, session, v);
|
||||||
com.minamp.net.Http.Response r = null;
|
com.minamp.net.Http.Response r = null;
|
||||||
try {
|
try {
|
||||||
r = com.minamp.net.Http.get(url, null, 30000);
|
r = com.minamp.net.Http.get(url, lib.server.mediaHeaders(), 30000);
|
||||||
if (r.code < 400) {
|
if (r.code < 400) {
|
||||||
byte[] buf = new byte[4096];
|
byte[] buf = new byte[4096];
|
||||||
int n = r.body.read(buf);
|
int n = r.body.read(buf);
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ public final class ImageLoader {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
if (app.server == null) return null;
|
if (app.server == null) return null;
|
||||||
String url = app.server.imageUrl(j.thumb, j.size);
|
String url = app.server.imageUrl(j.thumb, j.size);
|
||||||
Http.Response r = Http.get(url, null, 20000);
|
Http.Response r = Http.get(url, app.server.mediaHeaders(), 20000);
|
||||||
if (r.code != 200) { r.close(); return null; }
|
if (r.code != 200) { r.close(); return null; }
|
||||||
data = r.readAll();
|
data = r.readAll();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
Http.Response r = null;
|
Http.Response r = null;
|
||||||
String why = null;
|
String why = null;
|
||||||
for (int i = 0; i < urls.size(); i++) {
|
for (int i = 0; i < urls.size(); i++) {
|
||||||
r = Http.get(urls.get(i), Plex.headers(null), 30000);
|
r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000);
|
||||||
if (r.code == 200) {
|
if (r.code == 200) {
|
||||||
if (order != null) {
|
if (order != null) {
|
||||||
final int idx = i;
|
final int idx = i;
|
||||||
@@ -397,7 +397,8 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
refusalCode = 0;
|
refusalCode = 0;
|
||||||
acceptedVariant = -1;
|
acceptedVariant = -1;
|
||||||
// transcodes are fetched once and buffered, so player reconnects can't restart them
|
// transcodes are fetched once and buffered, so player reconnects can't restart them
|
||||||
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding));
|
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding,
|
||||||
|
App.get().server != null ? App.get().server.mediaHeaders() : null));
|
||||||
}
|
}
|
||||||
setState(BUFFERING, "Buffering…");
|
setState(BUFFERING, "Buffering…");
|
||||||
mp.prepareAsync();
|
mp.prepareAsync();
|
||||||
|
|||||||
@@ -160,7 +160,8 @@ public final class CertVerifier {
|
|||||||
GeneralName n = arr[i];
|
GeneralName n = arr[i];
|
||||||
if (n.getTagNo() == GeneralName.dNSName) {
|
if (n.getTagNo() == GeneralName.dNSName) {
|
||||||
sawDns = true;
|
sawDns = true;
|
||||||
if (!ip && dnsMatch(n.getName().toString(), host)) return true;
|
// String.valueOf, not getName().toString(): Dalvik < 4.0 can't call Object methods via an interface
|
||||||
|
if (!ip && dnsMatch(String.valueOf(n.getName()), host)) return true;
|
||||||
} else if (n.getTagNo() == GeneralName.iPAddress && ip) {
|
} else if (n.getTagNo() == GeneralName.iPAddress && ip) {
|
||||||
byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets();
|
byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets();
|
||||||
if (ipString(a).equals(host)) return true;
|
if (ipString(a).equals(host)) return true;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public final class StreamProxy implements Runnable {
|
|||||||
final String id;
|
final String id;
|
||||||
final List<String> urls;
|
final List<String> urls;
|
||||||
final boolean buffered;
|
final boolean buffered;
|
||||||
|
Map<String, String> headers; // sent upstream with every request
|
||||||
int chosen = -1; // index that worked
|
int chosen = -1; // index that worked
|
||||||
final List<Http.Response> live = new ArrayList<Http.Response>();
|
final List<Http.Response> live = new ArrayList<Http.Response>();
|
||||||
// buffered state (guarded by this)
|
// buffered state (guarded by this)
|
||||||
@@ -95,8 +96,14 @@ public final class StreamProxy implements Runnable {
|
|||||||
|
|
||||||
/** @return the local URL to give MediaPlayer, ending in an extension it may sniff. */
|
/** @return the local URL to give MediaPlayer, ending in an extension it may sniff. */
|
||||||
public String register(String id, List<String> remoteUrls, String ext, boolean buffered) {
|
public String register(String id, List<String> remoteUrls, String ext, boolean buffered) {
|
||||||
|
return register(id, remoteUrls, ext, buffered, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String register(String id, List<String> remoteUrls, String ext, boolean buffered, Map<String, String> headers) {
|
||||||
|
Route r = new Route(id, new ArrayList<String>(remoteUrls), buffered);
|
||||||
|
r.headers = headers;
|
||||||
synchronized (routes) {
|
synchronized (routes) {
|
||||||
routes.put(id, new Route(id, new ArrayList<String>(remoteUrls), buffered));
|
routes.put(id, r);
|
||||||
}
|
}
|
||||||
return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext;
|
return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext;
|
||||||
}
|
}
|
||||||
@@ -198,6 +205,7 @@ public final class StreamProxy implements Runnable {
|
|||||||
/** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */
|
/** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */
|
||||||
private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException {
|
private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException {
|
||||||
Map<String, String> h = new LinkedHashMap<String, String>();
|
Map<String, String> h = new LinkedHashMap<String, String>();
|
||||||
|
if (route.headers != null) h.putAll(route.headers);
|
||||||
h.put("Accept", "*/*");
|
h.put("Accept", "*/*");
|
||||||
if (range != null) h.put("Range", range);
|
if (range != null) h.put("Range", range);
|
||||||
String refusal = null;
|
String refusal = null;
|
||||||
@@ -216,6 +224,9 @@ public final class StreamProxy implements Runnable {
|
|||||||
up.close();
|
up.close();
|
||||||
code = up.code;
|
code = up.code;
|
||||||
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
|
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
|
||||||
|
if (up.code == 503 && route.urls.get(i).indexOf("/library/parts/") >= 0) {
|
||||||
|
refusal = "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?";
|
||||||
|
}
|
||||||
DiagLog.log("stream variant " + i + " refused: " + refusal);
|
DiagLog.log("stream variant " + i + " refused: " + refusal);
|
||||||
}
|
}
|
||||||
throw new RefusedException(code, refusal);
|
throw new RefusedException(code, refusal);
|
||||||
|
|||||||
@@ -251,12 +251,30 @@ public final class Library {
|
|||||||
return server.url(base + "?" + q);
|
return server.url(base + "?" + q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the server can actually read a track's file (Plex's checkFiles), for diagnostics. */
|
||||||
|
public String checkPart(Item t) {
|
||||||
|
try {
|
||||||
|
Json.Obj mc = server.get("/library/metadata/" + t.ratingKey + "?checkFiles=1");
|
||||||
|
Json.Arr md = mc == null ? null : mc.optArr("Metadata");
|
||||||
|
Json.Obj m = md == null ? null : md.optObj(0);
|
||||||
|
Json.Arr media = m == null ? null : m.optArr("Media");
|
||||||
|
Json.Obj me = media == null ? null : media.optObj(0);
|
||||||
|
Json.Arr parts = me == null ? null : me.optArr("Part");
|
||||||
|
Json.Obj p = parts == null ? null : parts.optObj(0);
|
||||||
|
if (p == null) return "no file information returned";
|
||||||
|
return "exists=" + p.optString("exists", "?") + " accessible=" + p.optString("accessible", "?")
|
||||||
|
+ " file=" + p.optString("file", "?");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return com.minamp.net.Threads.describe(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Ask the server what it would do with a transcode request (for the diagnostics screen). */
|
/** Ask the server what it would do with a transcode request (for the diagnostics screen). */
|
||||||
public String decision(Item t, int kbps, int variant) {
|
public String decision(Item t, int kbps, int variant) {
|
||||||
try {
|
try {
|
||||||
String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant);
|
String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant);
|
||||||
url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?");
|
url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?");
|
||||||
com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, Plex.headers(null), 20000);
|
com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, server.mediaHeaders(), 20000);
|
||||||
String body = r.readString();
|
String body = r.readString();
|
||||||
if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : "");
|
if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : "");
|
||||||
Json.Obj mc = Json.parseObj(body).optObj("MediaContainer");
|
Json.Obj mc = Json.parseObj(body).optObj("MediaContainer");
|
||||||
|
|||||||
@@ -73,7 +73,28 @@ public final class Server {
|
|||||||
|
|
||||||
public boolean isLocal() {
|
public boolean isLocal() {
|
||||||
Conn c = active;
|
Conn c = active;
|
||||||
return c != null && (c.kind == LOCAL || c.kind == LOCAL_HTTP);
|
if (c == null) return false;
|
||||||
|
if (c.kind == LOCAL || c.kind == LOCAL_HTTP) return true;
|
||||||
|
return c.kind == MANUAL && isPrivateAddress(c.uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A typed-in address on the home network (10.x, 172.16-31.x, 192.168.x, .local) counts as home. */
|
||||||
|
static boolean isPrivateAddress(String uri) {
|
||||||
|
String h = uri;
|
||||||
|
int s = h.indexOf("://");
|
||||||
|
if (s >= 0) h = h.substring(s + 3);
|
||||||
|
int end = h.length();
|
||||||
|
for (int i = 0; i < h.length(); i++) { char ch = h.charAt(i); if (ch == ':' || ch == '/') { end = i; break; } }
|
||||||
|
h = h.substring(0, end).toLowerCase();
|
||||||
|
if (h.endsWith(".local") || h.equals("localhost")) return true;
|
||||||
|
if (h.startsWith("10.") || h.startsWith("192.168.") || h.startsWith("127.")) return true;
|
||||||
|
if (h.startsWith("172.")) {
|
||||||
|
try {
|
||||||
|
int second = Integer.parseInt(h.substring(4, h.indexOf('.', 4)));
|
||||||
|
return second >= 16 && second <= 31;
|
||||||
|
} catch (Exception e) { return false; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRelay() {
|
public boolean isRelay() {
|
||||||
@@ -202,9 +223,17 @@ public final class Server {
|
|||||||
|
|
||||||
/** Absolute URL with the token in the query string (for the stream proxy and images). */
|
/** Absolute URL with the token in the query string (for the stream proxy and images). */
|
||||||
public String url(String path) throws IOException {
|
public String url(String path) throws IOException {
|
||||||
|
if (token == null || token.length() == 0) return base() + path; // never send an empty token
|
||||||
return base() + path + (path.indexOf('?') >= 0 ? "&" : "?") + "X-Plex-Token=" + Plex.enc(token);
|
return base() + path + (path.indexOf('?') >= 0 ? "&" : "?") + "X-Plex-Token=" + Plex.enc(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Headers for media requests (files, transcodes): Plex wants to know which client is asking. */
|
||||||
|
public java.util.Map<String, String> mediaHeaders() {
|
||||||
|
java.util.Map<String, String> h = Plex.headers(token);
|
||||||
|
h.put("Accept", "*/*");
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
public String imageUrl(String thumb, int size) throws IOException {
|
public String imageUrl(String thumb, int size) throws IOException {
|
||||||
if (thumb == null || thumb.length() == 0) return null;
|
if (thumb == null || thumb.length() == 0) return null;
|
||||||
return url("/photo/:/transcode?width=" + size + "&height=" + size + "&minSize=1&upscale=1&url=" + Plex.enc(thumb));
|
return url("/photo/:/transcode?width=" + size + "&height=" + size + "&minSize=1&upscale=1&url=" + Plex.enc(thumb));
|
||||||
|
|||||||
@@ -196,6 +196,32 @@ public class PlexTest {
|
|||||||
Server.Conn c3 = s3.pick();
|
Server.Conn c3 = s3.pick();
|
||||||
check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri);
|
check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri);
|
||||||
|
|
||||||
|
// --- manual LAN address without a token (server allows this network without sign-in)
|
||||||
|
Server lan = new Server();
|
||||||
|
lan.name = "manual"; lan.token = "";
|
||||||
|
lan.add(Server.normalizeManual("127.0.0.1"), Server.MANUAL);
|
||||||
|
lan.pick();
|
||||||
|
check(lan.isLocal(), "manual private address counts as home (original quality)");
|
||||||
|
check(lan.url("/library/parts/501/1/file.mp3").indexOf("X-Plex-Token") < 0, "no empty X-Plex-Token on media URLs");
|
||||||
|
Library lanLib = new Library(lan, "1");
|
||||||
|
Item lanTrack = lanLib.tracksOf(album).get(0);
|
||||||
|
String lid = proxy.newId();
|
||||||
|
String lurl = proxy.register(lid, java.util.Collections.singletonList(lanLib.directUrl(lanTrack)), "mp3", false, lan.mediaHeaders());
|
||||||
|
hc = (HttpURLConnection) new URL(lurl).openConnection(Proxy.NO_PROXY);
|
||||||
|
byte[] lb = read(hc.getInputStream());
|
||||||
|
check(hc.getResponseCode() == 200 && lb.length == 300000, "token-less LAN: original MP3 streams (" + hc.getResponseCode() + ")");
|
||||||
|
Item flac = lanLib.tracksOf(album).get(1);
|
||||||
|
String lid2 = proxy.newId();
|
||||||
|
String lurl2 = proxy.register(lid2, lanLib.transcodeUrls(flac, 192, 0, "lan1"), "mp3", true, lan.mediaHeaders());
|
||||||
|
hc = (HttpURLConnection) new URL(lurl2).openConnection(Proxy.NO_PROXY);
|
||||||
|
byte[] lt = read(hc.getInputStream());
|
||||||
|
check(hc.getResponseCode() == 200 && new String(lt, "ISO-8859-1").startsWith("TRANSCODE"), "token-less LAN: FLAC converts to MP3");
|
||||||
|
// and the old behaviour reproduces the phone's log exactly
|
||||||
|
Http.Response o1 = Http.get(lanLib.directUrl(lanTrack) + "?X-Plex-Token=", null, 10000); o1.readAll();
|
||||||
|
Http.Response o2 = Http.get(lanLib.transcodeUrls(flac, 192, 0, "lan2").get(0) + "&X-Plex-Token=", null, 10000); o2.readAll();
|
||||||
|
check(o1.code == 503 && o2.code == 400, "old empty-token URLs reproduce the log's 503 and 400");
|
||||||
|
check(lanLib.checkPart(lanTrack).indexOf("exists=") >= 0 || lanLib.checkPart(lanTrack).length() > 0, "file check runs: " + lanLib.checkPart(lanTrack));
|
||||||
|
|
||||||
// --- trust checks
|
// --- trust checks
|
||||||
CertVerifier.setRootStore(roots(args[1])); // real Mozilla roots: test CA must be rejected
|
CertVerifier.setRootStore(roots(args[1])); // real Mozilla roots: test CA must be rejected
|
||||||
Http.evictAll();
|
Http.evictAll();
|
||||||
|
|||||||
+11
-2
@@ -107,16 +107,25 @@ class H(BaseHTTPRequestHandler):
|
|||||||
if n:
|
if n:
|
||||||
self.rfile.read(n)
|
self.rfile.read(n)
|
||||||
u = urlparse(self.path)
|
u = urlparse(self.path)
|
||||||
q = parse_qs(u.query)
|
q = parse_qs(u.query, keep_blank_values=True)
|
||||||
p = u.path
|
p = u.path
|
||||||
host = self.headers.get("Host", "")
|
host = self.headers.get("Host", "")
|
||||||
tok = self.headers.get("X-Plex-Token") or (q.get("X-Plex-Token") or [None])[0]
|
tok = self.headers.get("X-Plex-Token") or (q.get("X-Plex-Token") or [None])[0]
|
||||||
|
empty_token_param = "X-Plex-Token" in q and q["X-Plex-Token"][0] == "" and not self.headers.get("X-Plex-Token")
|
||||||
log.append((method, host, self.path, tok, self.headers.get("Range")))
|
log.append((method, host, self.path, tok, self.headers.get("Range")))
|
||||||
if host.startswith("plex.tv"):
|
if host.startswith("plex.tv"):
|
||||||
return self.tv(method, p, tok)
|
return self.tv(method, p, tok)
|
||||||
if p == "/identity":
|
if p == "/identity":
|
||||||
return self.send_json({"MediaContainer": {"machineIdentifier": MACHINE, "version": "1.40"}})
|
return self.send_json({"MediaContainer": {"machineIdentifier": MACHINE, "version": "1.40"}})
|
||||||
if tok != TOKEN:
|
# Like a server with "allowed without auth" for its LAN: no token at all is fine from here,
|
||||||
|
# but media requests carrying an *empty* token are refused (503 files / 400 transcoder).
|
||||||
|
lan_no_auth = tok is None and self.server.server_address[1] == 32400
|
||||||
|
if empty_token_param:
|
||||||
|
if p.startswith("/library/parts/"):
|
||||||
|
return self.send_json({"error": "Service Unavailable"}, 503)
|
||||||
|
if "/transcode/" in p:
|
||||||
|
return self.send_json({"error": "Bad Request"}, 400)
|
||||||
|
if tok != TOKEN and not lan_no_auth:
|
||||||
return self.send_json({"error": "unauthorized"}, 401)
|
return self.send_json({"error": "unauthorized"}, 401)
|
||||||
if p == "/library/sections":
|
if p == "/library/sections":
|
||||||
return self.send_json({"MediaContainer": {"Directory": [
|
return self.send_json({"MediaContainer": {"Directory": [
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Rewrites Bouncy Castle calls that Android 2.x's Dalvik can't link.
|
||||||
|
|
||||||
|
Calling an Object method (toString/equals/hashCode/getClass) through an *interface* type compiles
|
||||||
|
to invokeinterface, which Dalvik before 4.0 can't resolve: NoSuchMethodError at runtime. Casting
|
||||||
|
the receiver to Object makes javac emit invokevirtual instead. tools/scan_dalvik.py checks nothing
|
||||||
|
was missed.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
FIXES = [
|
||||||
|
("asn1/ASN1InputStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"),
|
||||||
|
("asn1/ConstructedBitStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"),
|
||||||
|
("asn1/ConstructedOctetStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"),
|
||||||
|
("asn1/x509/AlgorithmIdentifier.java", "return pa.equals(pb);", "return ((Object) pa).equals(pb);"),
|
||||||
|
("asn1/x509/GeneralName.java", "buf.append(obj.toString());", "buf.append(((Object) obj).toString());"),
|
||||||
|
("crypto/CryptoServicesPermission.java", "return actions.toString();", "return ((Object) actions).toString();"),
|
||||||
|
("crypto/digests/SHA256Digest.java", "digest.getClass().getName()", "((Object) digest).getClass().getName()"),
|
||||||
|
("crypto/engines/AESEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
|
||||||
|
("crypto/engines/ARIAEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
|
||||||
|
("crypto/engines/DESedeEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
|
||||||
|
("crypto/engines/SM4Engine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
|
||||||
|
("crypto/modes/CCMBlockCipher.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
|
||||||
|
("math/ec/ECCurve.java", "getField().equals(other.getField())", "((Object) getField()).equals(other.getField())"),
|
||||||
|
("math/ec/ECCurve.java", "return getField().hashCode()", "return ((Object) getField()).hashCode()"),
|
||||||
|
("math/field/GenericPolynomialExtensionField.java", "return subfield.equals(other.subfield) && minimalPolynomial.equals(other.minimalPolynomial);",
|
||||||
|
"return ((Object) subfield).equals(other.subfield) && ((Object) minimalPolynomial).equals(other.minimalPolynomial);"),
|
||||||
|
("math/field/GenericPolynomialExtensionField.java", "return subfield.hashCode()", "return ((Object) subfield).hashCode()"),
|
||||||
|
("math/field/GenericPolynomialExtensionField.java", "rotateLeft(minimalPolynomial.hashCode(), 16)", "rotateLeft(((Object) minimalPolynomial).hashCode(), 16)"),
|
||||||
|
("tls/crypto/impl/AbstractTlsCrypto.java", "secret.getClass().getName()", "((Object) secret).getClass().getName()"),
|
||||||
|
("tls/crypto/impl/bc/BcTlsSecret.java", "secret.getClass().getName()", "((Object) secret).getClass().getName()"),
|
||||||
|
]
|
||||||
|
|
||||||
|
root = sys.argv[1].rstrip("/") + "/com/minamp/bc/"
|
||||||
|
for rel, old, new in FIXES:
|
||||||
|
p = root + rel
|
||||||
|
s = open(p).read()
|
||||||
|
if new in s:
|
||||||
|
continue
|
||||||
|
if old not in s:
|
||||||
|
raise SystemExit("pattern not found in %s: %s" % (rel, old))
|
||||||
|
open(p, "w").write(s.replace(old, new))
|
||||||
|
print("patched %d call sites" % len(FIXES))
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Find invokeinterface calls to java.lang.Object methods (toString/equals/hashCode/getClass) through an
|
||||||
|
# interface that doesn't declare them itself: Dalvik before 4.0 can't resolve those (NoSuchMethodError).
|
||||||
|
import os, re, subprocess, sys, zipfile
|
||||||
|
classes = sys.argv[1]; android_jar = sys.argv[2]
|
||||||
|
OBJ = {"toString:()Ljava/lang/String;", "equals:(Ljava/lang/Object;)Z", "hashCode:()I", "getClass:()Ljava/lang/Class;"}
|
||||||
|
z = zipfile.ZipFile(android_jar)
|
||||||
|
declared = {}
|
||||||
|
def declares(iface, m):
|
||||||
|
key = (iface, m)
|
||||||
|
if key in declared: return declared[key]
|
||||||
|
path = os.path.join(classes, iface + ".class")
|
||||||
|
src = path if os.path.exists(path) else None
|
||||||
|
args = ["javap", "-p", "-s", "-cp", classes if src else android_jar, iface.replace("/", ".")]
|
||||||
|
out = subprocess.run(args, capture_output=True, text=True).stdout
|
||||||
|
name, desc = m.split(":")
|
||||||
|
found = False
|
||||||
|
lines = out.splitlines()
|
||||||
|
for i, l in enumerate(lines):
|
||||||
|
if re.search(r"\b" + name + r"\(", l) and i + 1 < len(lines) and desc in lines[i + 1]:
|
||||||
|
found = True
|
||||||
|
# superinterfaces declaring it also count
|
||||||
|
if not found:
|
||||||
|
mm = re.search(r"interface \S+ extends (.+?) \{", out)
|
||||||
|
if mm:
|
||||||
|
for sup in mm.group(1).split(","):
|
||||||
|
sup = sup.strip().split("<")[0].replace(".", "/")
|
||||||
|
if sup and declares(sup, m): found = True
|
||||||
|
declared[key] = found
|
||||||
|
return found
|
||||||
|
hits = {}
|
||||||
|
files = [os.path.join(dp, f) for dp, _, fs in os.walk(classes) for f in fs if f.endswith(".class")]
|
||||||
|
for i in range(0, len(files), 200):
|
||||||
|
out = subprocess.run(["javap", "-c", "-p"] + files[i:i+200], capture_output=True, text=True).stdout
|
||||||
|
cur = None
|
||||||
|
for line in out.splitlines():
|
||||||
|
m = re.match(r"^(?:public |final |abstract |class |interface )*.*?(?:class|interface) (\S+)", line)
|
||||||
|
if m and not line.startswith(" "): cur = m.group(1)
|
||||||
|
mi = re.search(r"invokeinterface\s+#\d+,\s*\d+\s*// InterfaceMethod (\S+)\.(\w+):(\S+)", line)
|
||||||
|
if mi:
|
||||||
|
iface, name, desc = mi.group(1), mi.group(2), mi.group(3)
|
||||||
|
sig = name + ":" + desc
|
||||||
|
if sig in OBJ and not declares(iface, sig):
|
||||||
|
hits.setdefault((iface, sig), set()).add(cur)
|
||||||
|
for (iface, sig), users in sorted(hits.items()):
|
||||||
|
print(f"{iface}.{sig} <- {len(users)} class(es): " + ", ".join(sorted(users))[:300])
|
||||||
|
print("total patterns:", len(hits))
|
||||||
@@ -51,6 +51,7 @@ s = s.replace("return Long.divideUnsigned(x, y);", """if (y < 0) { return compar
|
|||||||
s = s.replace("return Long.parseUnsignedLong(s);", "return new java.math.BigInteger(s).longValue();")
|
s = s.replace("return Long.parseUnsignedLong(s);", "return new java.math.BigInteger(s).longValue();")
|
||||||
open(p, "w").write(s)
|
open(p, "w").write(s)
|
||||||
PY
|
PY
|
||||||
|
python3 "$PROJ/tools/patch_bc_dalvik.py" bcsrc
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== signing key"
|
echo "== signing key"
|
||||||
|
|||||||
Reference in new issue
Block a user