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:
xockoandClaude Opus 5.5 committed 2026-10-14 20:56:27 +02:00
1 parent a4b875ef34
commit 58d21b6d68
14 files changed
+218 -13

No files matched your search

+42
View File
@@ -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))
+46
View File
@@ -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))
+1
View File
@@ -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();")
open(p, "w").write(s)
PY
python3 "$PROJ/tools/patch_bc_dalvik.py" bcsrc
fi
echo "== signing key"