commit a4d756e6ba55c0be380118e5bd59a0de784c3c60 Author: Xosé Date: Sun Oct 18 19:03:33 2026 +0200 Add the shared TLS 1.2 client kit Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8aa755e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +test/out/ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..18e247b --- /dev/null +++ b/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copies the bundled-TLS network package into an app: ./install.sh +# e.g. ./install.sh ../freshmini org.freshmini.net +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +app=$1 pkg=$2 +dir=$app/src/$(echo "$pkg" | tr . /) +mkdir -p "$dir" "$app/assets" +for f in "$here"/net/*.java; do sed "s/^package com\.minamp\.net;/package $pkg;/" "$f" > "$dir/$(basename "$f")"; done +rm -rf "$app/assets/roots" && cp -r "$here/roots" "$app/assets/roots" +echo "installed $(ls "$dir" | wc -l | tr -d ' ') classes in $dir and $(ls "$app/assets/roots" | wc -l | tr -d ' ') roots" diff --git a/net/AssetRoots.java b/net/AssetRoots.java new file mode 100644 index 0000000..2b8d0d2 --- /dev/null +++ b/net/AssetRoots.java @@ -0,0 +1,50 @@ +package com.minamp.net; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.content.Context; + +/** Mozilla roots shipped as assets/roots/-.der */ +public final class AssetRoots implements CertVerifier.RootStore { + private final Context ctx; + private Set names; + + public AssetRoots(Context c) { ctx = c.getApplicationContext(); } + + public synchronized List find(byte[] subjectDer) { + try { + if (names == null) { + names = new HashSet(); + String[] l = ctx.getAssets().list("roots"); + if (l != null) for (int i = 0; i < l.length; i++) names.add(l[i]); + } + String h = CertVerifier.sha1Hex(subjectDer); + List out = null; + for (int i = 0; names.contains(h + "-" + i + ".der"); i++) { + if (out == null) out = new ArrayList(); + out.add(readAll(ctx.getAssets().open("roots/" + h + "-" + i + ".der"))); + } + return out; + } catch (IOException e) { + return null; + } + } + + static byte[] readAll(InputStream in) throws IOException { + try { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) b.write(buf, 0, n); + return b.toByteArray(); + } finally { + in.close(); + } + } +} diff --git a/net/CertVerifier.java b/net/CertVerifier.java new file mode 100644 index 0000000..547ae3a --- /dev/null +++ b/net/CertVerifier.java @@ -0,0 +1,331 @@ +package com.minamp.net; + +import java.io.IOException; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import com.minamp.bc.asn1.ASN1Encoding; +import com.minamp.bc.asn1.ASN1ObjectIdentifier; +import com.minamp.bc.asn1.ASN1OctetString; +import com.minamp.bc.asn1.pkcs.PKCSObjectIdentifiers; +import com.minamp.bc.asn1.x500.RDN; +import com.minamp.bc.asn1.x500.X500Name; +import com.minamp.bc.asn1.x500.style.BCStyle; +import com.minamp.bc.asn1.x500.style.IETFUtils; +import com.minamp.bc.asn1.x509.BasicConstraints; +import com.minamp.bc.asn1.x509.Certificate; +import com.minamp.bc.asn1.x509.Extension; +import com.minamp.bc.asn1.x509.Extensions; +import com.minamp.bc.asn1.x509.GeneralName; +import com.minamp.bc.asn1.x509.GeneralNames; +import com.minamp.bc.asn1.x9.X9ObjectIdentifiers; +import com.minamp.bc.crypto.Digest; +import com.minamp.bc.crypto.digests.SHA1Digest; +import com.minamp.bc.crypto.digests.SHA256Digest; +import com.minamp.bc.crypto.digests.SHA384Digest; +import com.minamp.bc.crypto.digests.SHA512Digest; +import com.minamp.bc.crypto.params.AsymmetricKeyParameter; +import com.minamp.bc.crypto.signers.DSADigestSigner; +import com.minamp.bc.crypto.signers.ECDSASigner; +import com.minamp.bc.crypto.signers.RSADigestSigner; +import com.minamp.bc.crypto.util.PublicKeyFactory; + +/** + * Minimal Web PKI path validation for the bundled TLS stack: + * hostname match, validity window, signature chain up to a bundled Mozilla root. + * Optionally one pinned leaf certificate (SHA-256) is trusted as-is, for self-signed servers. + * Shared with Minamp (tools/tlskit in the iOS ports' history); keep the copies in sync. + */ +public final class CertVerifier { + + /** Texts shown to the user; apps replace them with their own language. Formats take the host. */ + public static String msgMismatch = "The server certificate is for a different address (%1$s)."; + public static String msgDates = "The server certificate has expired, or the phone's date is wrong."; + + /** Thrown when the certificate is neither CA-trusted nor the pinned one: the app can offer to trust it. */ + public static final class Untrusted extends IOException { + public final String fingerprint, subject, issuer, expires; + public final boolean changed; // a pin was set and this is a different certificate + public final List chain; // as the server sent it, leaf first + + Untrusted(String fp, String subject, String issuer, String expires, boolean changed, List chain) { + super("untrusted certificate " + fp); + this.chain = chain; + this.fingerprint = fp; + this.subject = subject; + this.issuer = issuer; + this.expires = expires; + this.changed = changed; + } + } + + private static volatile String pin = ""; + + /** SHA-256 of the one leaf certificate to accept without a CA (hex, separators ignored); "" for none. */ + public static void setPin(String p) { + String n = norm(p); + if (!n.equals(pin)) { + pin = n; + Http.forgetSessions(); + } + } + + public static String norm(String s) { + if (s == null) return ""; + return s.replaceAll("[^0-9A-Fa-f]", "").toUpperCase(Locale.US); + } + + /** "AB:CD:..." SHA-256 of a DER certificate. */ + public static String sha256(byte[] der) { + try { + byte[] d = MessageDigest.getInstance("SHA-256").digest(der); + StringBuilder b = new StringBuilder(); + for (int i = 0; i < d.length; i++) { + if (i > 0) b.append(':'); + String x = Integer.toHexString(d[i] & 0xff).toUpperCase(Locale.US); + if (x.length() == 1) b.append('0'); + b.append(x); + } + return b.toString(); + } catch (Exception e) { + return ""; + } + } + + private static Untrusted untrusted(List chainDer, boolean changed) { + byte[] leafDer = chainDer.get(0); + String subject = "?", issuer = "?", expires = "?"; + try { + Certificate c = Certificate.getInstance(leafDer); + subject = String.valueOf(c.getSubject()); + issuer = String.valueOf(c.getIssuer()); + expires = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.US).format(c.getEndDate().getDate()); + } catch (Exception ignored) {} + return new Untrusted(sha256(leafDer), subject, issuer, expires, changed, chainDer); + } + + /** Lookup of trusted root certificates (DER) by the DER encoding of their subject name. */ + public interface RootStore { + List find(byte[] subjectDer); + } + + 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 verifiedChains = new java.util.HashSet(); + + /** 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 chainDer, String host) throws IOException { + if (chainDer == null || chainDer.isEmpty()) throw new IOException("Server sent no certificate"); + String p = pin; + if (p.length() > 0) { + // a pinned server: exactly that certificate, whatever its name or issuer + if (norm(sha256(chainDer.get(0))).equals(p)) return; + throw untrusted(chainDer, true); + } + 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(String.format(msgMismatch, host)); + if (new Date().getTime() > 1704067200000L) checkDates(leaf, new Date()); + return; + } + verifyFully(chainDer, host); + synchronized (verifiedChains) { verifiedChains.add(chainKey); } + } + + private static void verifyFully(List chainDer, String host) throws IOException { + final List chain0 = chainDer; + List chain = new ArrayList(); + for (int i = 0; i < chainDer.size(); i++) chain.add(Certificate.getInstance(chainDer.get(i))); + + Certificate leaf = chain.get(0); + if (!hostMatches(leaf, host)) { + throw new IOException(String.format(msgMismatch, host)); + } + + Date now = new Date(); + // An unset clock on an old phone (e.g. 1980 after a battery pull) would make every + // certificate look "not yet valid"; only enforce dates when the clock looks sane. + boolean clockSane = now.getTime() > 1704067200000L; // 2024-01-01 + + Certificate current = leaf; + for (int depth = 0; depth < 8; depth++) { + if (clockSane) checkDates(current, now); + + byte[] issuerDer = current.getIssuer().getEncoded(ASN1Encoding.DER); + + // 1. Anchored directly by a trusted root? + List anchors = roots.find(issuerDer); + if (anchors != null) { + byte[] currentDer = current.getEncoded(ASN1Encoding.DER); + for (int i = 0; i < anchors.size(); i++) { + // the very certificate the user chose to trust (e.g. self-signed) + if (java.util.Arrays.equals(anchors.get(i), currentDer)) return; + Certificate root = Certificate.getInstance(anchors.get(i)); + // anything that signs must be a CA: a trusted site's own certificate can't vouch for another + if (isCa(root) && signatureValid(current, root)) return; // trusted + } + } + + // 2. Otherwise find the issuing intermediate among what the server sent. + Certificate next = null; + for (int i = 1; i < chain.size(); i++) { + Certificate cand = chain.get(i); + if (cand == current) continue; + if (!java.util.Arrays.equals(cand.getSubject().getEncoded(ASN1Encoding.DER), issuerDer)) continue; + if (!isCa(cand)) continue; + if (signatureValid(current, cand)) { next = cand; break; } + } + if (next == null) throw untrusted(chain0, false); + current = next; + } + throw new IOException("Certificate chain too long"); + } + + private static void checkDates(Certificate c, Date now) throws IOException { + Date from = c.getStartDate().getDate(); + Date to = c.getEndDate().getDate(); + if (now.before(from) || now.after(to)) { + throw new IOException(msgDates); + } + } + + private static boolean isCa(Certificate c) { + Extensions ext = c.getTBSCertificate().getExtensions(); + if (ext == null) return c.getTBSCertificate().getVersionNumber() < 3; // v1 roots/intermediates + BasicConstraints bc = BasicConstraints.fromExtensions(ext); + return bc != null && bc.isCA(); + } + + static boolean signatureValid(Certificate subject, Certificate issuer) { + try { + ASN1ObjectIdentifier alg = subject.getSignatureAlgorithm().getAlgorithm(); + byte[] tbs = subject.getTBSCertificate().getEncoded(ASN1Encoding.DER); + byte[] sig = subject.getSignature().getOctets(); + AsymmetricKeyParameter key = PublicKeyFactory.createKey(issuer.getSubjectPublicKeyInfo()); + + if (alg.equals(PKCSObjectIdentifiers.sha256WithRSAEncryption)) return rsa(new SHA256Digest(), key, tbs, sig); + if (alg.equals(PKCSObjectIdentifiers.sha384WithRSAEncryption)) return rsa(new SHA384Digest(), key, tbs, sig); + if (alg.equals(PKCSObjectIdentifiers.sha512WithRSAEncryption)) return rsa(new SHA512Digest(), key, tbs, sig); + if (alg.equals(PKCSObjectIdentifiers.sha1WithRSAEncryption)) return rsa(new SHA1Digest(), key, tbs, sig); + if (alg.equals(X9ObjectIdentifiers.ecdsa_with_SHA256)) return ecdsa(new SHA256Digest(), key, tbs, sig); + if (alg.equals(X9ObjectIdentifiers.ecdsa_with_SHA384)) return ecdsa(new SHA384Digest(), key, tbs, sig); + if (alg.equals(X9ObjectIdentifiers.ecdsa_with_SHA512)) return ecdsa(new SHA512Digest(), key, tbs, sig); + return false; + } catch (Exception e) { + return false; + } + } + + private static boolean rsa(Digest d, AsymmetricKeyParameter key, byte[] tbs, byte[] sig) { + RSADigestSigner s = new RSADigestSigner(d); + s.init(false, key); + s.update(tbs, 0, tbs.length); + return s.verifySignature(sig); + } + + private static boolean ecdsa(Digest d, AsymmetricKeyParameter key, byte[] tbs, byte[] sig) { + DSADigestSigner s = new DSADigestSigner(new ECDSASigner(), d); + s.init(false, key); + s.update(tbs, 0, tbs.length); + return s.verifySignature(sig); + } + + // ---- hostname checking ---- + + static boolean hostMatches(Certificate leaf, String host) { + host = host.toLowerCase(Locale.US); + boolean ip = isIpLiteral(host); + boolean sawDns = false; + Extensions ext = leaf.getTBSCertificate().getExtensions(); + if (ext != null) { + GeneralNames names = GeneralNames.fromExtensions(ext, Extension.subjectAlternativeName); + if (names != null) { + GeneralName[] arr = names.getNames(); + for (int i = 0; i < arr.length; i++) { + GeneralName n = arr[i]; + if (n.getTagNo() == GeneralName.dNSName) { + sawDns = 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) { + byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets(); + if (ipString(a).equals(host)) return true; + } + } + } + } + if (!sawDns && !ip) { + X500Name subject = X500Name.getInstance(leaf.getSubject()); + RDN[] cns = subject.getRDNs(BCStyle.CN); + for (int i = 0; i < cns.length; i++) { + String cn = IETFUtils.valueToString(cns[i].getFirst().getValue()); + if (dnsMatch(cn, host)) return true; + } + } + return false; + } + + static boolean dnsMatch(String pattern, String host) { + pattern = pattern.toLowerCase(Locale.US); + if (pattern.endsWith(".")) pattern = pattern.substring(0, pattern.length() - 1); + if (host.endsWith(".")) host = host.substring(0, host.length() - 1); + if (pattern.startsWith("*.")) { + String suffix = pattern.substring(1); // ".example.com" + if (!host.endsWith(suffix)) return false; + String label = host.substring(0, host.length() - suffix.length()); + return label.length() > 0 && label.indexOf('.') < 0; + } + return pattern.equals(host); + } + + static boolean isIpLiteral(String host) { + if (host.indexOf(':') >= 0) return true; + for (int i = 0; i < host.length(); i++) { + char c = host.charAt(i); + if (!(c == '.' || (c >= '0' && c <= '9'))) return false; + } + return true; + } + + private static String ipString(byte[] a) { + if (a.length == 4) { + return (a[0] & 255) + "." + (a[1] & 255) + "." + (a[2] & 255) + "." + (a[3] & 255); + } + return ""; + } + + public static String sha1Hex(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] h = md.digest(data); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < h.length; i++) { + String s = Integer.toHexString(h[i] & 255); + if (s.length() == 1) sb.append('0'); + sb.append(s); + } + return sb.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/net/DiagLog.java b/net/DiagLog.java new file mode 100644 index 0000000..08e2b73 --- /dev/null +++ b/net/DiagLog.java @@ -0,0 +1,47 @@ +package com.minamp.net; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * In-memory diagnostics log (last few hundred lines): every request with its status and timing, + * sign-in steps, stream attempts. Viewable in the app and savable to the SD card, so a problem + * on a real server can be reported precisely. Tokens are never logged. + */ +public final class DiagLog { + private static final int MAX = 400; + private static final List lines = new ArrayList(); + private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss", Locale.US); + + private DiagLog() {} + + public static void log(String msg) { + String line; + synchronized (fmt) { line = fmt.format(new Date()) + " " + msg; } + synchronized (lines) { + lines.add(line); + if (lines.size() > MAX) lines.remove(0); + } + } + + public static String dump() { + StringBuilder sb = new StringBuilder(); + synchronized (lines) { + for (int i = 0; i < lines.size(); i++) sb.append(lines.get(i)).append('\n'); + } + return sb.toString(); + } + + public static void clear() { + synchronized (lines) { lines.clear(); } + } + + /** URL without the token value, safe to show and share. */ + public static String redact(String url) { + if (url == null) return ""; + return url.replaceAll("((?:X-Plex-Token|token|T|Passwd)=)[^&]*", "$1***"); + } +} diff --git a/net/Http.java b/net/Http.java new file mode 100644 index 0000000..72efe88 --- /dev/null +++ b/net/Http.java @@ -0,0 +1,588 @@ +package com.minamp.net; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Vector; + +import com.minamp.bc.tls.DefaultTlsClient; +import com.minamp.bc.tls.NameType; +import com.minamp.bc.tls.ServerName; +import com.minamp.bc.tls.TlsAuthentication; +import com.minamp.bc.tls.TlsClientProtocol; +import com.minamp.bc.tls.TlsCredentials; +import com.minamp.bc.tls.TlsServerCertificate; +import com.minamp.bc.tls.TlsSession; +import com.minamp.bc.tls.CertificateRequest; +import com.minamp.bc.tls.crypto.TlsCrypto; +import com.minamp.bc.tls.crypto.impl.bc.BcTlsCrypto; + +/** + * Small HTTP/1.1 client with its own TLS 1.2 (Bouncy Castle), keep-alive pooling and + * session resumption. Android 2.x's built-in SSL only speaks TLS 1.0 and lacks today's roots, + * so every request in the app goes through here. From Minamp; keep the copies in sync. + */ +public final class Http { + + public static String USER_AGENT = "Minamp/1.0"; + + /** Optional HTTP CONNECT proxy, only used when running the networking code on a desktop JVM. */ + public static String proxyHost; + public static int proxyPort; + + private static TlsCrypto crypto; + private static final Map sessions = new HashMap(); + private static final Map> pool = new HashMap>(); + private static final long IDLE_MS = 25000; + + private Http() {} + + private static synchronized TlsCrypto crypto() { + if (crypto == null) crypto = new BcTlsCrypto(new SecureRandom()); + return crypto; + } + + // ------------------------------------------------------------------ URL + + public static final class Url { + public final boolean https; + public final String host; + public final int port; + public final String path; // includes query + + public Url(String s) throws IOException { + String lower = s.toLowerCase(Locale.US); + int start; + if (lower.startsWith("https://")) { https = true; start = 8; } + else if (lower.startsWith("http://")) { https = false; start = 7; } + else throw new IOException("Bad URL: " + s); + int slash = s.indexOf('/', start); + String hostPort = slash < 0 ? s.substring(start) : s.substring(start, slash); + path = slash < 0 ? "/" : s.substring(slash); + int colon = hostPort.lastIndexOf(':'); + if (colon > 0 && hostPort.indexOf(']') < colon) { + host = hostPort.substring(0, colon); + try { port = Integer.parseInt(hostPort.substring(colon + 1)); } + catch (NumberFormatException e) { throw new IOException("Bad port in " + s); } + } else { + host = hostPort; + port = https ? 443 : 80; + } + } + + String key() { return (https ? "s:" : "p:") + host + ":" + port; } + + String hostHeader() { + boolean dflt = (https && port == 443) || (!https && port == 80); + return dflt ? host : host + ":" + port; + } + } + + // ------------------------------------------------------------------ connection + + static final class Conn { + final String key; + Socket socket; + InputStream in; + OutputStream out; + TlsClientProtocol tls; + long idleSince; + + Conn(String key) { this.key = key; } + + void close() { + try { if (tls != null) tls.close(); } catch (Exception ignored) {} + try { if (socket != null) socket.close(); } catch (Exception ignored) {} + } + } + + private static Conn take(String key) { + synchronized (pool) { + List list = pool.get(key); + if (list == null) return null; + long now = System.currentTimeMillis(); + while (!list.isEmpty()) { + Conn c = list.remove(list.size() - 1); + if (now - c.idleSince < IDLE_MS && !c.socket.isClosed()) return c; + c.close(); + } + return null; + } + } + + static void release(Conn c) { + c.idleSince = System.currentTimeMillis(); + synchronized (pool) { + List list = pool.get(c.key); + if (list == null) { list = new ArrayList(); pool.put(c.key, list); } + if (list.size() >= 4) { list.remove(0).close(); } + list.add(c); + } + } + + /** 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 doomed = new ArrayList(); + synchronized (pool) { + for (Iterator> it = pool.values().iterator(); it.hasNext();) doomed.addAll(it.next()); + pool.clear(); + } + if (doomed.isEmpty()) return; + // closing TLS sends an encrypted alert: keep that off the (small-stack) caller thread + Threads.start(new Runnable() { + public void run() { for (int i = 0; i < doomed.size(); i++) doomed.get(i).close(); } + }, "http-evict"); + } + + private static Conn connect(final Url u, int timeoutMs) throws IOException { + Conn c = new Conn(u.key()); + Socket s = new Socket(); + boolean ok = false; + try { + if (proxyHost != null) { + s.connect(new InetSocketAddress(proxyHost, proxyPort), timeoutMs); + s.setSoTimeout(timeoutMs); + OutputStream po = s.getOutputStream(); + String req = "CONNECT " + u.host + ":" + u.port + " HTTP/1.1\r\nHost: " + u.host + ":" + u.port + "\r\n\r\n"; + po.write(req.getBytes("ISO-8859-1")); + po.flush(); + InputStream pi = s.getInputStream(); + String status = readLine(pi); + if (status == null || status.indexOf(" 200") < 0) throw new IOException("Proxy refused: " + status); + String line; + while ((line = readLine(pi)) != null && line.length() > 0) { /* skip headers */ } + } else { + s.connect(new InetSocketAddress(u.host, u.port), timeoutMs); + s.setSoTimeout(timeoutMs); + } + s.setTcpNoDelay(true); + c.socket = s; + if (u.https) { + TlsClientProtocol proto = new TlsClientProtocol(s.getInputStream(), s.getOutputStream()); + proto.connect(new Client(crypto(), u.host, u.key())); + c.tls = proto; + c.in = new BufferedInputStream(proto.getInputStream(), 8192); + c.out = new BufferedOutputStream(proto.getOutputStream(), 4096); + } else { + c.in = new BufferedInputStream(s.getInputStream(), 8192); + c.out = new BufferedOutputStream(s.getOutputStream(), 4096); + } + ok = true; + return c; + } finally { + if (!ok) { try { s.close(); } catch (Exception ignored) {} } + } + } + + private static final class Client extends DefaultTlsClient { + 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; + this.sessionKey = sessionKey; + } + + @Override + protected Vector getSNIServerNames() { + if (CertVerifier.isIpLiteral(host)) return null; + Vector v = new Vector(); + try { + v.addElement(new ServerName(NameType.host_name, host.getBytes("US-ASCII"))); + } catch (java.io.UnsupportedEncodingException e) { + return null; + } + 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); } + } + + @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); } + } + } + + public TlsAuthentication getAuthentication() throws IOException { + return new TlsAuthentication() { + public void notifyServerCertificate(TlsServerCertificate sc) throws IOException { + com.minamp.bc.tls.Certificate cert = sc.getCertificate(); + List chain = new ArrayList(); + for (int i = 0; i < cert.getLength(); i++) chain.add(cert.getCertificateAt(i).getEncoded()); + CertVerifier.verify(chain, host); + } + + public TlsCredentials getClientCredentials(CertificateRequest req) { + return null; + } + }; + } + } + + // ------------------------------------------------------------------ request / response + + public static final class Response { + public final int code; + public final String status; + public final Map headers; // lower-case names + public final InputStream body; + public final long contentLength; // -1 if unknown + public final boolean chunked; + + Response(int code, String status, Map headers, InputStream body, long len, boolean chunked) { + this.code = code; this.status = status; this.headers = headers; this.body = body; + this.contentLength = len; this.chunked = chunked; + } + + public String header(String name) { return headers.get(name.toLowerCase(Locale.US)); } + + public void close() { + try { body.close(); } catch (IOException ignored) {} + } + + public byte[] readAll() throws IOException { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(contentLength > 0 && contentLength < 4000000 ? (int) contentLength : 8192); + byte[] buf = new byte[8192]; + int n; + while ((n = body.read(buf)) > 0) bos.write(buf, 0, n); + return bos.toByteArray(); + } finally { + close(); + } + } + + public String readString() throws IOException { + return new String(readAll(), "UTF-8"); + } + } + + public static Response get(String url, Map headers, int timeoutMs) throws IOException { + return request("GET", url, headers, null, timeoutMs); + } + + public static Response request(String method, String url, Map headers, byte[] body, int timeoutMs) throws IOException { + return request(method, url, headers, body, timeoutMs, true); + } + + /** A single request; 3xx responses are returned to the caller. */ + public static Response requestNoRedirect(String method, String url, Map headers, byte[] body, int timeoutMs) throws IOException { + return request(method, url, headers, body, timeoutMs, false); + } + + private static Response request(String method, String url, Map headers, byte[] body, int timeoutMs, boolean follow) throws IOException { + for (int redirects = 0; ; redirects++) { + long t0 = System.currentTimeMillis(); + Response r; + try { + r = requestOnce(method, url, headers, body, timeoutMs); + } catch (IOException e) { + DiagLog.log(method + " " + DiagLog.redact(url) + " -> FAILED after " + (System.currentTimeMillis() - t0) + "ms: " + Threads.describe(e)); + throw e; + } catch (RuntimeException e) { + DiagLog.log(method + " " + DiagLog.redact(url) + " -> FAILED: " + Threads.describe(e)); + throw e; + } + if (!url.contains("/photo/:/transcode") && !url.contains("/cover")) { // artwork would drown out everything else + DiagLog.log(method + " " + DiagLog.redact(url) + " -> " + r.code + " (" + (System.currentTimeMillis() - t0) + "ms)"); + } + if (follow && (r.code == 301 || r.code == 302 || r.code == 307 || r.code == 308) && redirects < 4 + && ("GET".equals(method) || "HEAD".equals(method))) { + String loc = r.header("location"); + r.close(); + if (loc == null) throw new IOException("Redirect without location"); + if (loc.startsWith("/")) { + Url u = new Url(url); + loc = (u.https ? "https://" : "http://") + u.hostHeader() + loc; + } + url = loc; + continue; + } + return r; + } + } + + private static Response requestOnce(String method, String url, Map headers, byte[] body, int timeoutMs) throws IOException { + Url u = new Url(url); + // A pooled connection may have been closed by the server; retry once on a fresh one. + for (int attempt = 0; attempt < 2; attempt++) { + Conn c = take(u.key()); + boolean pooled = c != null; + if (c == null) c = connect(u, timeoutMs); + try { + c.socket.setSoTimeout(timeoutMs); + writeRequest(c, u, method, headers, body); + return readResponse(c, method); + } catch (IOException e) { + c.close(); + if (!pooled) throw e; + } + } + throw new IOException("Request failed"); + } + + private static void writeRequest(Conn c, Url u, String method, Map headers, byte[] body) throws IOException { + StringBuilder sb = new StringBuilder(512); + sb.append(method).append(' ').append(u.path).append(" HTTP/1.1\r\n"); + sb.append("Host: ").append(u.hostHeader()).append("\r\n"); + boolean hasUa = false; + if (headers != null) { + for (Map.Entry e : headers.entrySet()) { + if (e.getValue() == null) continue; + if ("user-agent".equalsIgnoreCase(e.getKey())) hasUa = true; + sb.append(e.getKey()).append(": ").append(e.getValue()).append("\r\n"); + } + } + if (!hasUa) sb.append("User-Agent: ").append(USER_AGENT).append("\r\n"); + sb.append("Connection: keep-alive\r\n"); + if (body != null || "POST".equals(method) || "PUT".equals(method)) { + sb.append("Content-Length: ").append(body == null ? 0 : body.length).append("\r\n"); + } + sb.append("\r\n"); + c.out.write(sb.toString().getBytes("UTF-8")); + if (body != null) c.out.write(body); + c.out.flush(); + } + + private static Response readResponse(Conn c, String method) throws IOException { + String statusLine = readLine(c.in); + if (statusLine == null) throw new IOException("Connection closed"); + // HTTP/1.1 200 OK + int sp = statusLine.indexOf(' '); + if (sp < 0) throw new IOException("Bad status line"); + int sp2 = statusLine.indexOf(' ', sp + 1); + int code; + try { + code = Integer.parseInt(sp2 < 0 ? statusLine.substring(sp + 1).trim() : statusLine.substring(sp + 1, sp2)); + } catch (NumberFormatException e) { + throw new IOException("Bad status line"); + } + boolean http10 = statusLine.startsWith("HTTP/1.0"); + Map h = new LinkedHashMap(); + String line; + while ((line = readLine(c.in)) != null && line.length() > 0) { + int colon = line.indexOf(':'); + if (colon <= 0) continue; + String name = line.substring(0, colon).trim().toLowerCase(Locale.US); + String val = line.substring(colon + 1).trim(); + String prev = h.get(name); + h.put(name, prev == null ? val : prev + ", " + val); + } + String te = h.get("transfer-encoding"); + String cl = h.get("content-length"); + String conn = h.get("connection"); + boolean keepAlive = !http10 && (conn == null || conn.toLowerCase(Locale.US).indexOf("close") < 0); + boolean chunked = te != null && te.toLowerCase(Locale.US).indexOf("chunked") >= 0; + long len = -1; + if (!chunked && cl != null) { + try { len = Long.parseLong(cl.trim()); } catch (NumberFormatException ignored) {} + } + InputStream body; + boolean noBody = "HEAD".equals(method) || code == 204 || code == 304 || (code >= 100 && code < 200); + if (noBody) { + body = new FixedStream(c, 0, keepAlive); + len = "HEAD".equals(method) ? len : 0; + } else if (chunked) { + body = new ChunkedStream(c, keepAlive); + } else if (len >= 0) { + body = new FixedStream(c, len, keepAlive); + } else { + body = new EofStream(c); + } + return new Response(code, statusLine, h, body, len, chunked); + } + + static String readLine(InputStream in) throws IOException { + StringBuilder sb = new StringBuilder(64); + int b; + while ((b = in.read()) >= 0) { + if (b == '\n') { + int n = sb.length(); + if (n > 0 && sb.charAt(n - 1) == '\r') sb.setLength(n - 1); + return sb.toString(); + } + sb.append((char) b); + if (sb.length() > 16384) throw new IOException("Header line too long"); + } + return sb.length() == 0 ? null : sb.toString(); + } + + // ------------------------------------------------------------------ body streams + + private static final class FixedStream extends InputStream { + private final Conn c; private long left; private final boolean keepAlive; private boolean closed; + + FixedStream(Conn c, long len, boolean keepAlive) { + this.c = c; this.left = len; this.keepAlive = keepAlive; + if (len == 0) finish(true); + } + + private void finish(boolean complete) { + if (closed) return; + closed = true; + if (complete && keepAlive) release(c); else c.close(); + } + + @Override public int read() throws IOException { + byte[] one = new byte[1]; + int n = read(one, 0, 1); + return n <= 0 ? -1 : one[0] & 255; + } + + @Override public int read(byte[] b, int off, int len) throws IOException { + if (left <= 0) { finish(true); return -1; } + if (closed) return -1; + int n = c.in.read(b, off, (int) Math.min(len, left)); + if (n < 0) { finish(false); throw new IOException("Unexpected end of stream"); } + left -= n; + if (left == 0) finish(true); + return n; + } + + @Override public int available() throws IOException { + if (closed) return 0; + return (int) Math.min(c.in.available(), left); + } + + @Override public void close() { finish(left == 0); } + } + + private static final class ChunkedStream extends InputStream { + private final Conn c; private final boolean keepAlive; + private long chunkLeft = 0; private boolean eof; private boolean closed; + + ChunkedStream(Conn c, boolean keepAlive) { this.c = c; this.keepAlive = keepAlive; } + + private void finish(boolean complete) { + if (closed) return; + closed = true; + if (complete && keepAlive) release(c); else c.close(); + } + + private boolean nextChunk() throws IOException { + if (eof) return false; + String line = readLine(c.in); + if (line == null) throw new IOException("Unexpected end of chunked stream"); + if (line.length() == 0) { line = readLine(c.in); if (line == null) throw new IOException("Bad chunk"); } + int semi = line.indexOf(';'); + if (semi >= 0) line = line.substring(0, semi); + try { chunkLeft = Long.parseLong(line.trim(), 16); } + catch (NumberFormatException e) { throw new IOException("Bad chunk size"); } + if (chunkLeft == 0) { + String t; + while ((t = readLine(c.in)) != null && t.length() > 0) { /* trailers */ } + eof = true; + finish(true); + return false; + } + return true; + } + + @Override public int read() throws IOException { + byte[] one = new byte[1]; + int n = read(one, 0, 1); + return n <= 0 ? -1 : one[0] & 255; + } + + @Override public int read(byte[] b, int off, int len) throws IOException { + if (closed) return -1; + if (chunkLeft == 0) { + if (!nextChunk()) return -1; + } + int n = c.in.read(b, off, (int) Math.min(len, chunkLeft)); + if (n < 0) { finish(false); throw new IOException("Unexpected end of chunk"); } + chunkLeft -= n; + if (chunkLeft == 0) { + // consume CRLF after chunk data + String crlf = readLine(c.in); + if (crlf == null) { finish(false); } + else if (crlf.length() != 0) { finish(false); throw new IOException("Bad chunk terminator"); } + } + return n; + } + + @Override public int available() throws IOException { + if (closed) return 0; + return (int) Math.min(c.in.available(), chunkLeft); + } + + @Override public void close() { finish(eof); } + } + + private static final class EofStream extends InputStream { + private final Conn c; private boolean closed; + EofStream(Conn c) { this.c = c; } + + @Override public int read() throws IOException { + if (closed) return -1; + int b = c.in.read(); + if (b < 0) close(); + return b; + } + + @Override public int read(byte[] b, int off, int len) throws IOException { + if (closed) return -1; + int n = c.in.read(b, off, len); + if (n < 0) close(); + return n; + } + + @Override public void close() { + if (!closed) { closed = true; c.close(); } + } + } + + /** Close one in-flight response's socket from another thread (used to abort streams). */ + public static void abort(Response r) { + if (r == null) return; + 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) {} + } + } +} diff --git a/net/Threads.java b/net/Threads.java new file mode 100644 index 0000000..c96be3b --- /dev/null +++ b/net/Threads.java @@ -0,0 +1,43 @@ +package com.minamp.net; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Background threads with big stacks. Android 2.x gives new threads ~16 KB of stack, and the + * bundled TLS handshake + certificate parsing needs far more; overflowing it kills the app. + * 256 KB is the most Dalvik allows. + */ +public final class Threads { + public static final long STACK = 256 * 1024; + + private Threads() {} + + public static Thread create(Runnable r, String name) { + Thread t = new Thread(null, r, name, STACK); + t.setDaemon(true); + return t; + } + + public static void start(Runnable r, String name) { + create(r, name).start(); + } + + public static ExecutorService pool(final String name, int n) { + ThreadFactory f = new ThreadFactory() { + private int count; + public synchronized Thread newThread(Runnable r) { return create(r, name + "-" + (++count)); } + }; + return n == 1 ? Executors.newSingleThreadExecutor(f) : Executors.newFixedThreadPool(n, f); + } + + /** Readable one-liner for any failure, including Errors such as StackOverflowError. */ + public static String describe(Throwable t) { + String m = t.getMessage(); + String cls = t.getClass().getSimpleName(); + if (m == null || m.length() == 0) return cls; + if (t instanceof java.io.IOException) return m; + return cls + ": " + m; + } +} diff --git a/roots/032d371dbf31afaf7ded8c4817d515f8152ed5a6-0.der b/roots/032d371dbf31afaf7ded8c4817d515f8152ed5a6-0.der new file mode 100644 index 0000000..47bfc6f Binary files /dev/null and b/roots/032d371dbf31afaf7ded8c4817d515f8152ed5a6-0.der differ diff --git a/roots/03eb97b4e8a522a943a90d3301b19f99a8e6fcaf-0.der b/roots/03eb97b4e8a522a943a90d3301b19f99a8e6fcaf-0.der new file mode 100644 index 0000000..cb9a586 Binary files /dev/null and b/roots/03eb97b4e8a522a943a90d3301b19f99a8e6fcaf-0.der differ diff --git a/roots/0c0ed0137e724ee890170d0a0797ca406897614d-0.der b/roots/0c0ed0137e724ee890170d0a0797ca406897614d-0.der new file mode 100644 index 0000000..1dd645c Binary files /dev/null and b/roots/0c0ed0137e724ee890170d0a0797ca406897614d-0.der differ diff --git a/roots/0f4efcf05175b60e4914344f85fcf18b3f826fd8-0.der b/roots/0f4efcf05175b60e4914344f85fcf18b3f826fd8-0.der new file mode 100644 index 0000000..c4347d3 Binary files /dev/null and b/roots/0f4efcf05175b60e4914344f85fcf18b3f826fd8-0.der differ diff --git a/roots/0f5b4ff6887b4722adb4859609e6a925212174e0-0.der b/roots/0f5b4ff6887b4722adb4859609e6a925212174e0-0.der new file mode 100644 index 0000000..7031f88 Binary files /dev/null and b/roots/0f5b4ff6887b4722adb4859609e6a925212174e0-0.der differ diff --git a/roots/108abf716ef774bd28f3e2cd3949cc2eaa52316a-0.der b/roots/108abf716ef774bd28f3e2cd3949cc2eaa52316a-0.der new file mode 100644 index 0000000..a0cd3ec Binary files /dev/null and b/roots/108abf716ef774bd28f3e2cd3949cc2eaa52316a-0.der differ diff --git a/roots/1336799a38ebd57730aa49f89be555490ab47bf8-0.der b/roots/1336799a38ebd57730aa49f89be555490ab47bf8-0.der new file mode 100644 index 0000000..ae5ae35 Binary files /dev/null and b/roots/1336799a38ebd57730aa49f89be555490ab47bf8-0.der differ diff --git a/roots/14146f28d9038bfbf04a2093c205456baf4034b2-0.der b/roots/14146f28d9038bfbf04a2093c205456baf4034b2-0.der new file mode 100644 index 0000000..96e02ca Binary files /dev/null and b/roots/14146f28d9038bfbf04a2093c205456baf4034b2-0.der differ diff --git a/roots/14c0f884650ebfb1564d3da153a0d7f1c7c670a8-0.der b/roots/14c0f884650ebfb1564d3da153a0d7f1c7c670a8-0.der new file mode 100644 index 0000000..cf6588c Binary files /dev/null and b/roots/14c0f884650ebfb1564d3da153a0d7f1c7c670a8-0.der differ diff --git a/roots/17a7d72a82ce8c8116af6a12f0701505259f816d-0.der b/roots/17a7d72a82ce8c8116af6a12f0701505259f816d-0.der new file mode 100644 index 0000000..f63cc7f Binary files /dev/null and b/roots/17a7d72a82ce8c8116af6a12f0701505259f816d-0.der differ diff --git a/roots/1ca566add30938e8135505ffd08b5a70c488b73b-0.der b/roots/1ca566add30938e8135505ffd08b5a70c488b73b-0.der new file mode 100644 index 0000000..d1531f6 Binary files /dev/null and b/roots/1ca566add30938e8135505ffd08b5a70c488b73b-0.der differ diff --git a/roots/1d236f8e064b971fdddfa11523e1aee502be2b1b-0.der b/roots/1d236f8e064b971fdddfa11523e1aee502be2b1b-0.der new file mode 100644 index 0000000..3cdad66 Binary files /dev/null and b/roots/1d236f8e064b971fdddfa11523e1aee502be2b1b-0.der differ diff --git a/roots/20dd24a4280ef8840064866941b494f3f30de194-0.der b/roots/20dd24a4280ef8840064866941b494f3f30de194-0.der new file mode 100644 index 0000000..7508480 Binary files /dev/null and b/roots/20dd24a4280ef8840064866941b494f3f30de194-0.der differ diff --git a/roots/220f44b97081f27eb2d32757ef0242567e51fed9-0.der b/roots/220f44b97081f27eb2d32757ef0242567e51fed9-0.der new file mode 100644 index 0000000..fed8dab Binary files /dev/null and b/roots/220f44b97081f27eb2d32757ef0242567e51fed9-0.der differ diff --git a/roots/229a8437e3d9edafce3dc9904897b48fdeb436d3-0.der b/roots/229a8437e3d9edafce3dc9904897b48fdeb436d3-0.der new file mode 100644 index 0000000..6eb01cc Binary files /dev/null and b/roots/229a8437e3d9edafce3dc9904897b48fdeb436d3-0.der differ diff --git a/roots/254d769250bbe91e2178c18a2240319eaf17e365-0.der b/roots/254d769250bbe91e2178c18a2240319eaf17e365-0.der new file mode 100644 index 0000000..354b346 Binary files /dev/null and b/roots/254d769250bbe91e2178c18a2240319eaf17e365-0.der differ diff --git a/roots/262175b10d1adadd3525515deb67dda91edf2154-0.der b/roots/262175b10d1adadd3525515deb67dda91edf2154-0.der new file mode 100644 index 0000000..5a3dc50 Binary files /dev/null and b/roots/262175b10d1adadd3525515deb67dda91edf2154-0.der differ diff --git a/roots/281aea4e6a11200e3949b766237385489c2e8792-0.der b/roots/281aea4e6a11200e3949b766237385489c2e8792-0.der new file mode 100644 index 0000000..9d2132e Binary files /dev/null and b/roots/281aea4e6a11200e3949b766237385489c2e8792-0.der differ diff --git a/roots/2c7a348b4c3fe39c4b2f2a1919788781b2fde745-0.der b/roots/2c7a348b4c3fe39c4b2f2a1919788781b2fde745-0.der new file mode 100644 index 0000000..2913bb0 Binary files /dev/null and b/roots/2c7a348b4c3fe39c4b2f2a1919788781b2fde745-0.der differ diff --git a/roots/3053402c7b5dbe592758be81f8b1a0c89bd9db40-0.der b/roots/3053402c7b5dbe592758be81f8b1a0c89bd9db40-0.der new file mode 100644 index 0000000..45ac938 Binary files /dev/null and b/roots/3053402c7b5dbe592758be81f8b1a0c89bd9db40-0.der differ diff --git a/roots/3091c2d60f82ea189edb13b0755d9b046e67f050-0.der b/roots/3091c2d60f82ea189edb13b0755d9b046e67f050-0.der new file mode 100644 index 0000000..e193a05 Binary files /dev/null and b/roots/3091c2d60f82ea189edb13b0755d9b046e67f050-0.der differ diff --git a/roots/31e4c31bde81d32ad0d8ecacd40c3da4e71aeb38-0.der b/roots/31e4c31bde81d32ad0d8ecacd40c3da4e71aeb38-0.der new file mode 100644 index 0000000..f81b63a Binary files /dev/null and b/roots/31e4c31bde81d32ad0d8ecacd40c3da4e71aeb38-0.der differ diff --git a/roots/322ad42b3fe752585a7c1435fd71b9763f2c7bd9-0.der b/roots/322ad42b3fe752585a7c1435fd71b9763f2c7bd9-0.der new file mode 100644 index 0000000..a8b2d8e Binary files /dev/null and b/roots/322ad42b3fe752585a7c1435fd71b9763f2c7bd9-0.der differ diff --git a/roots/3366ae4363646a8c15060a3b4adff12e8c1809af-0.der b/roots/3366ae4363646a8c15060a3b4adff12e8c1809af-0.der new file mode 100644 index 0000000..02ab364 Binary files /dev/null and b/roots/3366ae4363646a8c15060a3b4adff12e8c1809af-0.der differ diff --git a/roots/3582a277b57e01b32d31d926b4f7b4366493d235-0.der b/roots/3582a277b57e01b32d31d926b4f7b4366493d235-0.der new file mode 100644 index 0000000..62d354a Binary files /dev/null and b/roots/3582a277b57e01b32d31d926b4f7b4366493d235-0.der differ diff --git a/roots/36341f8404085341117cfe8fc657667384b14135-0.der b/roots/36341f8404085341117cfe8fc657667384b14135-0.der new file mode 100644 index 0000000..e5e2343 Binary files /dev/null and b/roots/36341f8404085341117cfe8fc657667384b14135-0.der differ diff --git a/roots/369df24e274d52c0b46a58590043b883b4f6b096-0.der b/roots/369df24e274d52c0b46a58590043b883b4f6b096-0.der new file mode 100644 index 0000000..40db2f3 Binary files /dev/null and b/roots/369df24e274d52c0b46a58590043b883b4f6b096-0.der differ diff --git a/roots/39d28b71fe1d19b65fb3f1288f23bc04595c4395-0.der b/roots/39d28b71fe1d19b65fb3f1288f23bc04595c4395-0.der new file mode 100644 index 0000000..1e927a7 Binary files /dev/null and b/roots/39d28b71fe1d19b65fb3f1288f23bc04595c4395-0.der differ diff --git a/roots/3f1855d3c9a93b24f8b4f95a07ae048c20cea42d-0.der b/roots/3f1855d3c9a93b24f8b4f95a07ae048c20cea42d-0.der new file mode 100644 index 0000000..2f6e59f Binary files /dev/null and b/roots/3f1855d3c9a93b24f8b4f95a07ae048c20cea42d-0.der differ diff --git a/roots/41655b67500e9a2e2c0cdcba7a9247f773f77269-0.der b/roots/41655b67500e9a2e2c0cdcba7a9247f773f77269-0.der new file mode 100644 index 0000000..3e49eba Binary files /dev/null and b/roots/41655b67500e9a2e2c0cdcba7a9247f773f77269-0.der differ diff --git a/roots/41fc0249061723abd565d5bce6f9562e79c7c8e8-0.der b/roots/41fc0249061723abd565d5bce6f9562e79c7c8e8-0.der new file mode 100644 index 0000000..6160479 Binary files /dev/null and b/roots/41fc0249061723abd565d5bce6f9562e79c7c8e8-0.der differ diff --git a/roots/4264562253e3c442d08ec17f32fb2d58f406ace3-0.der b/roots/4264562253e3c442d08ec17f32fb2d58f406ace3-0.der new file mode 100644 index 0000000..71f9f88 Binary files /dev/null and b/roots/4264562253e3c442d08ec17f32fb2d58f406ace3-0.der differ diff --git a/roots/42aaaa5ea487b4f18a25daaf86f22533b7ee39ab-0.der b/roots/42aaaa5ea487b4f18a25daaf86f22533b7ee39ab-0.der new file mode 100644 index 0000000..db0e85a Binary files /dev/null and b/roots/42aaaa5ea487b4f18a25daaf86f22533b7ee39ab-0.der differ diff --git a/roots/42d0b7e6a82098efcfb77957c139770ef5df885f-0.der b/roots/42d0b7e6a82098efcfb77957c139770ef5df885f-0.der new file mode 100644 index 0000000..e7ad806 Binary files /dev/null and b/roots/42d0b7e6a82098efcfb77957c139770ef5df885f-0.der differ diff --git a/roots/481f557ae1206403ecc941543a54504072f2c1df-0.der b/roots/481f557ae1206403ecc941543a54504072f2c1df-0.der new file mode 100644 index 0000000..d46aa38 Binary files /dev/null and b/roots/481f557ae1206403ecc941543a54504072f2c1df-0.der differ diff --git a/roots/4a7cdaf39dfbf67800700df49cd84af1531b0969-0.der b/roots/4a7cdaf39dfbf67800700df49cd84af1531b0969-0.der new file mode 100644 index 0000000..1449f86 Binary files /dev/null and b/roots/4a7cdaf39dfbf67800700df49cd84af1531b0969-0.der differ diff --git a/roots/4b1ec7d2704f8539cb63fb2d863f09b26393e823-0.der b/roots/4b1ec7d2704f8539cb63fb2d863f09b26393e823-0.der new file mode 100644 index 0000000..70a9ec9 Binary files /dev/null and b/roots/4b1ec7d2704f8539cb63fb2d863f09b26393e823-0.der differ diff --git a/roots/4e9b3837a2d6eb46985ae7cc638568fdaced325b-0.der b/roots/4e9b3837a2d6eb46985ae7cc638568fdaced325b-0.der new file mode 100644 index 0000000..3bbe4a9 Binary files /dev/null and b/roots/4e9b3837a2d6eb46985ae7cc638568fdaced325b-0.der differ diff --git a/roots/4f59a39453cfb9559e7f6bd8c54da53da642b714-0.der b/roots/4f59a39453cfb9559e7f6bd8c54da53da642b714-0.der new file mode 100644 index 0000000..86b7dcd Binary files /dev/null and b/roots/4f59a39453cfb9559e7f6bd8c54da53da642b714-0.der differ diff --git a/roots/538933b9a76ad7c55cbf4dc2d4769ce2bb50d20b-0.der b/roots/538933b9a76ad7c55cbf4dc2d4769ce2bb50d20b-0.der new file mode 100644 index 0000000..603c65e Binary files /dev/null and b/roots/538933b9a76ad7c55cbf4dc2d4769ce2bb50d20b-0.der differ diff --git a/roots/5588e1fad8ea116388ec542a0110b63fb5c826e6-0.der b/roots/5588e1fad8ea116388ec542a0110b63fb5c826e6-0.der new file mode 100644 index 0000000..ecd8c9d Binary files /dev/null and b/roots/5588e1fad8ea116388ec542a0110b63fb5c826e6-0.der differ diff --git a/roots/55f003ec26723b65ec4b936600e5d1af9fa80fa9-0.der b/roots/55f003ec26723b65ec4b936600e5d1af9fa80fa9-0.der new file mode 100644 index 0000000..91d8e47 Binary files /dev/null and b/roots/55f003ec26723b65ec4b936600e5d1af9fa80fa9-0.der differ diff --git a/roots/5cd9dcebfc1a5bb0aa6923e532de48a56851f8ec-0.der b/roots/5cd9dcebfc1a5bb0aa6923e532de48a56851f8ec-0.der new file mode 100644 index 0000000..10ef441 Binary files /dev/null and b/roots/5cd9dcebfc1a5bb0aa6923e532de48a56851f8ec-0.der differ diff --git a/roots/5e021b686c5cd3be1691995789dfc41472163d03-0.der b/roots/5e021b686c5cd3be1691995789dfc41472163d03-0.der new file mode 100644 index 0000000..83fdeb7 Binary files /dev/null and b/roots/5e021b686c5cd3be1691995789dfc41472163d03-0.der differ diff --git a/roots/62fbb4c96c7627f44989f56d3795e701ecff86ed-0.der b/roots/62fbb4c96c7627f44989f56d3795e701ecff86ed-0.der new file mode 100644 index 0000000..3492b95 Binary files /dev/null and b/roots/62fbb4c96c7627f44989f56d3795e701ecff86ed-0.der differ diff --git a/roots/6435ac18333cfe8ca49f61b4f2151d005ee86ce9-0.der b/roots/6435ac18333cfe8ca49f61b4f2151d005ee86ce9-0.der new file mode 100644 index 0000000..7a991b7 Binary files /dev/null and b/roots/6435ac18333cfe8ca49f61b4f2151d005ee86ce9-0.der differ diff --git a/roots/64f5c98fa750327769f72ce76ccfd9ea956526f3-0.der b/roots/64f5c98fa750327769f72ce76ccfd9ea956526f3-0.der new file mode 100644 index 0000000..fa25f67 Binary files /dev/null and b/roots/64f5c98fa750327769f72ce76ccfd9ea956526f3-0.der differ diff --git a/roots/69ad4bafb45c7a8423a7fb924545dff35aab46eb-0.der b/roots/69ad4bafb45c7a8423a7fb924545dff35aab46eb-0.der new file mode 100644 index 0000000..a59a90b Binary files /dev/null and b/roots/69ad4bafb45c7a8423a7fb924545dff35aab46eb-0.der differ diff --git a/roots/6cf8311aae93dc3feadb1abc64d44c38494b2940-0.der b/roots/6cf8311aae93dc3feadb1abc64d44c38494b2940-0.der new file mode 100644 index 0000000..aac1822 Binary files /dev/null and b/roots/6cf8311aae93dc3feadb1abc64d44c38494b2940-0.der differ diff --git a/roots/6f43c3357b52a21924146518f9cb7d4dbe05a82f-0.der b/roots/6f43c3357b52a21924146518f9cb7d4dbe05a82f-0.der new file mode 100644 index 0000000..5aea734 Binary files /dev/null and b/roots/6f43c3357b52a21924146518f9cb7d4dbe05a82f-0.der differ diff --git a/roots/7036af3c2bea54342640b3ff78c70768e0fdfd68-0.der b/roots/7036af3c2bea54342640b3ff78c70768e0fdfd68-0.der new file mode 100644 index 0000000..316a530 Binary files /dev/null and b/roots/7036af3c2bea54342640b3ff78c70768e0fdfd68-0.der differ diff --git a/roots/704b80e92c15138ab8bef8cad0f2ed793ab3697c-0.der b/roots/704b80e92c15138ab8bef8cad0f2ed793ab3697c-0.der new file mode 100644 index 0000000..235e186 Binary files /dev/null and b/roots/704b80e92c15138ab8bef8cad0f2ed793ab3697c-0.der differ diff --git a/roots/71719c5ecfe502b0b8bdccfe3aac3b0057a13809-0.der b/roots/71719c5ecfe502b0b8bdccfe3aac3b0057a13809-0.der new file mode 100644 index 0000000..22d99a4 Binary files /dev/null and b/roots/71719c5ecfe502b0b8bdccfe3aac3b0057a13809-0.der differ diff --git a/roots/724dba45ab521e8115a29d695c1fc06daee1671c-0.der b/roots/724dba45ab521e8115a29d695c1fc06daee1671c-0.der new file mode 100644 index 0000000..63ce66e Binary files /dev/null and b/roots/724dba45ab521e8115a29d695c1fc06daee1671c-0.der differ diff --git a/roots/72c74580524922e9a5b62e238cb16cd887402dde-0.der b/roots/72c74580524922e9a5b62e238cb16cd887402dde-0.der new file mode 100644 index 0000000..47d3400 Binary files /dev/null and b/roots/72c74580524922e9a5b62e238cb16cd887402dde-0.der differ diff --git a/roots/79e688bd141d71bbcfed90e833ca29c89162f156-0.der b/roots/79e688bd141d71bbcfed90e833ca29c89162f156-0.der new file mode 100644 index 0000000..24e5adb Binary files /dev/null and b/roots/79e688bd141d71bbcfed90e833ca29c89162f156-0.der differ diff --git a/roots/79eabcd4a06fa193dda974a7c6cf184c1fbb4f14-0.der b/roots/79eabcd4a06fa193dda974a7c6cf184c1fbb4f14-0.der new file mode 100644 index 0000000..e99af2e Binary files /dev/null and b/roots/79eabcd4a06fa193dda974a7c6cf184c1fbb4f14-0.der differ diff --git a/roots/7e63ed24cfd532db5d6e66acee1149168947a20d-0.der b/roots/7e63ed24cfd532db5d6e66acee1149168947a20d-0.der new file mode 100644 index 0000000..70cd462 Binary files /dev/null and b/roots/7e63ed24cfd532db5d6e66acee1149168947a20d-0.der differ diff --git a/roots/7f7fad1e1daeeb75e7ecc9ad1ec5f590f5b98461-0.der b/roots/7f7fad1e1daeeb75e7ecc9ad1ec5f590f5b98461-0.der new file mode 100644 index 0000000..b40648c Binary files /dev/null and b/roots/7f7fad1e1daeeb75e7ecc9ad1ec5f590f5b98461-0.der differ diff --git a/roots/81abd26e80718923cd527a3315639bcabc82baac-0.der b/roots/81abd26e80718923cd527a3315639bcabc82baac-0.der new file mode 100644 index 0000000..d0e4cf3 Binary files /dev/null and b/roots/81abd26e80718923cd527a3315639bcabc82baac-0.der differ diff --git a/roots/83cf671cc5fe6cef98a1dba2ad2bdd787607c611-0.der b/roots/83cf671cc5fe6cef98a1dba2ad2bdd787607c611-0.der new file mode 100644 index 0000000..056eace Binary files /dev/null and b/roots/83cf671cc5fe6cef98a1dba2ad2bdd787607c611-0.der differ diff --git a/roots/83fcdde3ff2a744eb4370369af62cd497095e486-0.der b/roots/83fcdde3ff2a744eb4370369af62cd497095e486-0.der new file mode 100644 index 0000000..452e549 Binary files /dev/null and b/roots/83fcdde3ff2a744eb4370369af62cd497095e486-0.der differ diff --git a/roots/843eb9be7c5fcf0311180cf2f1e3ecb9d5e36027-0.der b/roots/843eb9be7c5fcf0311180cf2f1e3ecb9d5e36027-0.der new file mode 100644 index 0000000..cbccb43 Binary files /dev/null and b/roots/843eb9be7c5fcf0311180cf2f1e3ecb9d5e36027-0.der differ diff --git a/roots/887da4445e67ea7c94774e43189c3ecee4c87312-0.der b/roots/887da4445e67ea7c94774e43189c3ecee4c87312-0.der new file mode 100644 index 0000000..42f3c1b Binary files /dev/null and b/roots/887da4445e67ea7c94774e43189c3ecee4c87312-0.der differ diff --git a/roots/8a938d24845c923b512b1cd0ac0e7a64a33b4cf0-0.der b/roots/8a938d24845c923b512b1cd0ac0e7a64a33b4cf0-0.der new file mode 100644 index 0000000..0f5f95f Binary files /dev/null and b/roots/8a938d24845c923b512b1cd0ac0e7a64a33b4cf0-0.der differ diff --git a/roots/8aa2eaae98f84c6b1703fefce5ee0bc9afed2fa6-0.der b/roots/8aa2eaae98f84c6b1703fefce5ee0bc9afed2fa6-0.der new file mode 100644 index 0000000..c568023 Binary files /dev/null and b/roots/8aa2eaae98f84c6b1703fefce5ee0bc9afed2fa6-0.der differ diff --git a/roots/8df6bca2fa8dd4f6a68eaf461bf2091e9aa77576-0.der b/roots/8df6bca2fa8dd4f6a68eaf461bf2091e9aa77576-0.der new file mode 100644 index 0000000..1a0c2e2 Binary files /dev/null and b/roots/8df6bca2fa8dd4f6a68eaf461bf2091e9aa77576-0.der differ diff --git a/roots/8e88f59059604b03df43780a8094a649cdb975f6-0.der b/roots/8e88f59059604b03df43780a8094a649cdb975f6-0.der new file mode 100644 index 0000000..d0e8f0b Binary files /dev/null and b/roots/8e88f59059604b03df43780a8094a649cdb975f6-0.der differ diff --git a/roots/9370a55f7c274e0d3a10562231c60a406a8d98a6-0.der b/roots/9370a55f7c274e0d3a10562231c60a406a8d98a6-0.der new file mode 100644 index 0000000..6927c52 Binary files /dev/null and b/roots/9370a55f7c274e0d3a10562231c60a406a8d98a6-0.der differ diff --git a/roots/953e4462a2852855675a4a4f8a790c5e904cfa5a-0.der b/roots/953e4462a2852855675a4a4f8a790c5e904cfa5a-0.der new file mode 100644 index 0000000..09438c8 Binary files /dev/null and b/roots/953e4462a2852855675a4a4f8a790c5e904cfa5a-0.der differ diff --git a/roots/96db2813a1d464fed7792b0db6d29e5d8308bbf9-0.der b/roots/96db2813a1d464fed7792b0db6d29e5d8308bbf9-0.der new file mode 100644 index 0000000..a4dfe1f Binary files /dev/null and b/roots/96db2813a1d464fed7792b0db6d29e5d8308bbf9-0.der differ diff --git a/roots/9717ccde7d8296f32c79363127c7d366c7ccf94c-0.der b/roots/9717ccde7d8296f32c79363127c7d366c7ccf94c-0.der new file mode 100644 index 0000000..12f481e Binary files /dev/null and b/roots/9717ccde7d8296f32c79363127c7d366c7ccf94c-0.der differ diff --git a/roots/984a0a63fba1d7069deb8931aef39c24ea7a5c47-0.der b/roots/984a0a63fba1d7069deb8931aef39c24ea7a5c47-0.der new file mode 100644 index 0000000..d618f1e Binary files /dev/null and b/roots/984a0a63fba1d7069deb8931aef39c24ea7a5c47-0.der differ diff --git a/roots/9b55f064e2c8498d97481114eeaf435fe0cd3a39-0.der b/roots/9b55f064e2c8498d97481114eeaf435fe0cd3a39-0.der new file mode 100644 index 0000000..16a364d Binary files /dev/null and b/roots/9b55f064e2c8498d97481114eeaf435fe0cd3a39-0.der differ diff --git a/roots/9bbd65196c3a9fffbf0bc4bfdb20299bf0d6df8f-0.der b/roots/9bbd65196c3a9fffbf0bc4bfdb20299bf0d6df8f-0.der new file mode 100644 index 0000000..58d018a Binary files /dev/null and b/roots/9bbd65196c3a9fffbf0bc4bfdb20299bf0d6df8f-0.der differ diff --git a/roots/9be242c83e15138e38e53cc701da3ab52d475144-0.der b/roots/9be242c83e15138e38e53cc701da3ab52d475144-0.der new file mode 100644 index 0000000..6a62de1 Binary files /dev/null and b/roots/9be242c83e15138e38e53cc701da3ab52d475144-0.der differ diff --git a/roots/a27cf103f952a9ba8cd636012f4adfffe82b92af-0.der b/roots/a27cf103f952a9ba8cd636012f4adfffe82b92af-0.der new file mode 100644 index 0000000..e4795f3 Binary files /dev/null and b/roots/a27cf103f952a9ba8cd636012f4adfffe82b92af-0.der differ diff --git a/roots/a4c867427bbd6f1c2a429387154ec72447e68508-0.der b/roots/a4c867427bbd6f1c2a429387154ec72447e68508-0.der new file mode 100644 index 0000000..c1258ee Binary files /dev/null and b/roots/a4c867427bbd6f1c2a429387154ec72447e68508-0.der differ diff --git a/roots/a4d7c276adf4f0fa5b7b3606db53807f3cd79d09-0.der b/roots/a4d7c276adf4f0fa5b7b3606db53807f3cd79d09-0.der new file mode 100644 index 0000000..c676be5 Binary files /dev/null and b/roots/a4d7c276adf4f0fa5b7b3606db53807f3cd79d09-0.der differ diff --git a/roots/a76e38b989c88f14809cb8826da96b75cadeeb8d-0.der b/roots/a76e38b989c88f14809cb8826da96b75cadeeb8d-0.der new file mode 100644 index 0000000..1ff0c9c Binary files /dev/null and b/roots/a76e38b989c88f14809cb8826da96b75cadeeb8d-0.der differ diff --git a/roots/aab38011e10133cb69cdf7f710e973cf9577abbf-0.der b/roots/aab38011e10133cb69cdf7f710e973cf9577abbf-0.der new file mode 100644 index 0000000..8c59853 Binary files /dev/null and b/roots/aab38011e10133cb69cdf7f710e973cf9577abbf-0.der differ diff --git a/roots/abf377b2272de78e18d5a67e5b1297dce8c5dac1-0.der b/roots/abf377b2272de78e18d5a67e5b1297dce8c5dac1-0.der new file mode 100644 index 0000000..9c53496 Binary files /dev/null and b/roots/abf377b2272de78e18d5a67e5b1297dce8c5dac1-0.der differ diff --git a/roots/b0e31e6fe1b4e58b38cd4664dd9184b2eead11f6-0.der b/roots/b0e31e6fe1b4e58b38cd4664dd9184b2eead11f6-0.der new file mode 100644 index 0000000..194de84 Binary files /dev/null and b/roots/b0e31e6fe1b4e58b38cd4664dd9184b2eead11f6-0.der differ diff --git a/roots/b63a54aeb7c096b2c72835b60f7d423e851e811a-0.der b/roots/b63a54aeb7c096b2c72835b60f7d423e851e811a-0.der new file mode 100644 index 0000000..dd740ab Binary files /dev/null and b/roots/b63a54aeb7c096b2c72835b60f7d423e851e811a-0.der differ diff --git a/roots/b7e7d918696e6d071cafa898ed1bf22538e71ab2-0.der b/roots/b7e7d918696e6d071cafa898ed1bf22538e71ab2-0.der new file mode 100644 index 0000000..53919a6 Binary files /dev/null and b/roots/b7e7d918696e6d071cafa898ed1bf22538e71ab2-0.der differ diff --git a/roots/badf8ae3f7eb508c94c1bae31e7cdc3a713d4437-0.der b/roots/badf8ae3f7eb508c94c1bae31e7cdc3a713d4437-0.der new file mode 100644 index 0000000..d6aa0f4 Binary files /dev/null and b/roots/badf8ae3f7eb508c94c1bae31e7cdc3a713d4437-0.der differ diff --git a/roots/bece0f14569371822ea32fed5faa588deb9a03a2-0.der b/roots/bece0f14569371822ea32fed5faa588deb9a03a2-0.der new file mode 100644 index 0000000..0642a03 Binary files /dev/null and b/roots/bece0f14569371822ea32fed5faa588deb9a03a2-0.der differ diff --git a/roots/c17e0d0305d965b2bf298a6c492a3b0bea8ba25f-0.der b/roots/c17e0d0305d965b2bf298a6c492a3b0bea8ba25f-0.der new file mode 100644 index 0000000..86d70df Binary files /dev/null and b/roots/c17e0d0305d965b2bf298a6c492a3b0bea8ba25f-0.der differ diff --git a/roots/c5c7806f3b0ec8c5ac7df8fd9922dcc8b15bd0bf-0.der b/roots/c5c7806f3b0ec8c5ac7df8fd9922dcc8b15bd0bf-0.der new file mode 100644 index 0000000..0911a56 Binary files /dev/null and b/roots/c5c7806f3b0ec8c5ac7df8fd9922dcc8b15bd0bf-0.der differ diff --git a/roots/c90a620226764c614af7a3e7549b6b1dac6b0f55-0.der b/roots/c90a620226764c614af7a3e7549b6b1dac6b0f55-0.der new file mode 100644 index 0000000..675cbeb Binary files /dev/null and b/roots/c90a620226764c614af7a3e7549b6b1dac6b0f55-0.der differ diff --git a/roots/c9770b343f6c16083e2a9e9be4f9ee2de35dd3d9-0.der b/roots/c9770b343f6c16083e2a9e9be4f9ee2de35dd3d9-0.der new file mode 100644 index 0000000..982345d Binary files /dev/null and b/roots/c9770b343f6c16083e2a9e9be4f9ee2de35dd3d9-0.der differ diff --git a/roots/cc74fc3960063bc493f2540de8ccc3b47e59d84a-0.der b/roots/cc74fc3960063bc493f2540de8ccc3b47e59d84a-0.der new file mode 100644 index 0000000..86658ae Binary files /dev/null and b/roots/cc74fc3960063bc493f2540de8ccc3b47e59d84a-0.der differ diff --git a/roots/cd30d24c343a82ab1f0570158ad7a107762992e9-0.der b/roots/cd30d24c343a82ab1f0570158ad7a107762992e9-0.der new file mode 100644 index 0000000..2fa45b2 Binary files /dev/null and b/roots/cd30d24c343a82ab1f0570158ad7a107762992e9-0.der differ diff --git a/roots/cf3ab99d14f558ddad1db1c0c87dd6b76685ff6b-0.der b/roots/cf3ab99d14f558ddad1db1c0c87dd6b76685ff6b-0.der new file mode 100644 index 0000000..26f98a8 Binary files /dev/null and b/roots/cf3ab99d14f558ddad1db1c0c87dd6b76685ff6b-0.der differ diff --git a/roots/d07c43a2a0f778fed080dff8600e49515c1d05c2-0.der b/roots/d07c43a2a0f778fed080dff8600e49515c1d05c2-0.der new file mode 100644 index 0000000..ff212ef Binary files /dev/null and b/roots/d07c43a2a0f778fed080dff8600e49515c1d05c2-0.der differ diff --git a/roots/d422b47d649377b28a8d0620f4ffafe3444e309c-0.der b/roots/d422b47d649377b28a8d0620f4ffafe3444e309c-0.der new file mode 100644 index 0000000..7a8caf9 Binary files /dev/null and b/roots/d422b47d649377b28a8d0620f4ffafe3444e309c-0.der differ diff --git a/roots/d83a4cebac21908f4f278d95db510429928526b9-0.der b/roots/d83a4cebac21908f4f278d95db510429928526b9-0.der new file mode 100644 index 0000000..cf29ccc Binary files /dev/null and b/roots/d83a4cebac21908f4f278d95db510429928526b9-0.der differ diff --git a/roots/db967dfe40104bbb8a15f44c45067d6d9ca0117a-0.der b/roots/db967dfe40104bbb8a15f44c45067d6d9ca0117a-0.der new file mode 100644 index 0000000..339a109 Binary files /dev/null and b/roots/db967dfe40104bbb8a15f44c45067d6d9ca0117a-0.der differ diff --git a/roots/dd8ae898b225382efd64f9f3c37486534fa9cbda-0.der b/roots/dd8ae898b225382efd64f9f3c37486534fa9cbda-0.der new file mode 100644 index 0000000..3f6981b Binary files /dev/null and b/roots/dd8ae898b225382efd64f9f3c37486534fa9cbda-0.der differ diff --git a/roots/ddb36391a7eefac7df3e47972cdd9c04be806e9d-0.der b/roots/ddb36391a7eefac7df3e47972cdd9c04be806e9d-0.der new file mode 100644 index 0000000..b21a344 Binary files /dev/null and b/roots/ddb36391a7eefac7df3e47972cdd9c04be806e9d-0.der differ diff --git a/roots/de1d43c47af5f1bc86ab36437e021703f89ccf5c-0.der b/roots/de1d43c47af5f1bc86ab36437e021703f89ccf5c-0.der new file mode 100644 index 0000000..56039c0 Binary files /dev/null and b/roots/de1d43c47af5f1bc86ab36437e021703f89ccf5c-0.der differ diff --git a/roots/df22cf8b8c3b46c10d3d5c407561eabeb57f8181-0.der b/roots/df22cf8b8c3b46c10d3d5c407561eabeb57f8181-0.der new file mode 100644 index 0000000..99bcc84 Binary files /dev/null and b/roots/df22cf8b8c3b46c10d3d5c407561eabeb57f8181-0.der differ diff --git a/roots/e24e78259c056c7f870c7b62f5b043d96fab7725-0.der b/roots/e24e78259c056c7f870c7b62f5b043d96fab7725-0.der new file mode 100644 index 0000000..9519275 Binary files /dev/null and b/roots/e24e78259c056c7f870c7b62f5b043d96fab7725-0.der differ diff --git a/roots/e7e9ceba6df839282ba96295895cca12e56e2cd6-0.der b/roots/e7e9ceba6df839282ba96295895cca12e56e2cd6-0.der new file mode 100644 index 0000000..f61e2f8 Binary files /dev/null and b/roots/e7e9ceba6df839282ba96295895cca12e56e2cd6-0.der differ diff --git a/roots/e9c6f2faacfba8533a5d58a04d4f76d1fa532b0b-0.der b/roots/e9c6f2faacfba8533a5d58a04d4f76d1fa532b0b-0.der new file mode 100644 index 0000000..f88e810 Binary files /dev/null and b/roots/e9c6f2faacfba8533a5d58a04d4f76d1fa532b0b-0.der differ diff --git a/roots/eb25d890fe2720f41d7d6ac15eef76cf62cfab78-0.der b/roots/eb25d890fe2720f41d7d6ac15eef76cf62cfab78-0.der new file mode 100644 index 0000000..9e42698 Binary files /dev/null and b/roots/eb25d890fe2720f41d7d6ac15eef76cf62cfab78-0.der differ diff --git a/roots/eb8ebc9d472b7e0291771923f2a5f1c1c51e57b5-0.der b/roots/eb8ebc9d472b7e0291771923f2a5f1c1c51e57b5-0.der new file mode 100644 index 0000000..6dda6a3 Binary files /dev/null and b/roots/eb8ebc9d472b7e0291771923f2a5f1c1c51e57b5-0.der differ diff --git a/roots/ef58da4b2284f3860c68b18b916a064a520a9728-0.der b/roots/ef58da4b2284f3860c68b18b916a064a520a9728-0.der new file mode 100644 index 0000000..9c1e26c Binary files /dev/null and b/roots/ef58da4b2284f3860c68b18b916a064a520a9728-0.der differ diff --git a/roots/f026813c6ea2e5bbca921b4b884a50f107f0cbff-0.der b/roots/f026813c6ea2e5bbca921b4b884a50f107f0cbff-0.der new file mode 100644 index 0000000..507b842 Binary files /dev/null and b/roots/f026813c6ea2e5bbca921b4b884a50f107f0cbff-0.der differ diff --git a/roots/f180fc731db5ec5a468e4ba9bc8ab856319ceda9-0.der b/roots/f180fc731db5ec5a468e4ba9bc8ab856319ceda9-0.der new file mode 100644 index 0000000..72e48c3 Binary files /dev/null and b/roots/f180fc731db5ec5a468e4ba9bc8ab856319ceda9-0.der differ diff --git a/roots/f20ef3e3718f19d130d9cc7a2dc303161cb29cd0-0.der b/roots/f20ef3e3718f19d130d9cc7a2dc303161cb29cd0-0.der new file mode 100644 index 0000000..a42b519 Binary files /dev/null and b/roots/f20ef3e3718f19d130d9cc7a2dc303161cb29cd0-0.der differ diff --git a/roots/f33f2ef9470df6a3ca848f278ea48015299c54f1-0.der b/roots/f33f2ef9470df6a3ca848f278ea48015299c54f1-0.der new file mode 100644 index 0000000..767b3c2 Binary files /dev/null and b/roots/f33f2ef9470df6a3ca848f278ea48015299c54f1-0.der differ diff --git a/roots/f4ee867f8a28dc57b3dfe2c74facf594b917520b-0.der b/roots/f4ee867f8a28dc57b3dfe2c74facf594b917520b-0.der new file mode 100644 index 0000000..b29eefd Binary files /dev/null and b/roots/f4ee867f8a28dc57b3dfe2c74facf594b917520b-0.der differ diff --git a/roots/f5042d4a785f7dab303cd2dc815d90dee6995f09-0.der b/roots/f5042d4a785f7dab303cd2dc815d90dee6995f09-0.der new file mode 100644 index 0000000..6a336d4 Binary files /dev/null and b/roots/f5042d4a785f7dab303cd2dc815d90dee6995f09-0.der differ diff --git a/roots/f59639270d6a2019f1683191887f7f3edb1f2013-0.der b/roots/f59639270d6a2019f1683191887f7f3edb1f2013-0.der new file mode 100644 index 0000000..d5f8989 Binary files /dev/null and b/roots/f59639270d6a2019f1683191887f7f3edb1f2013-0.der differ diff --git a/roots/f59c687f2418d62a790f7592330756ea85e94707-0.der b/roots/f59c687f2418d62a790f7592330756ea85e94707-0.der new file mode 100644 index 0000000..232c4b6 Binary files /dev/null and b/roots/f59c687f2418d62a790f7592330756ea85e94707-0.der differ diff --git a/roots/f7025660b8bbc206931faf24a7161c03980f6301-0.der b/roots/f7025660b8bbc206931faf24a7161c03980f6301-0.der new file mode 100644 index 0000000..e18997f Binary files /dev/null and b/roots/f7025660b8bbc206931faf24a7161c03980f6301-0.der differ diff --git a/roots/f85cf300817f2b6cd6f5d220fd224155c466d35b-0.der b/roots/f85cf300817f2b6cd6f5d220fd224155c466d35b-0.der new file mode 100644 index 0000000..c55cb68 Binary files /dev/null and b/roots/f85cf300817f2b6cd6f5d220fd224155c466d35b-0.der differ diff --git a/roots/f9780538aed47baadb39e8710440f329a43a372a-0.der b/roots/f9780538aed47baadb39e8710440f329a43a372a-0.der new file mode 100644 index 0000000..3a08309 Binary files /dev/null and b/roots/f9780538aed47baadb39e8710440f329a43a372a-0.der differ diff --git a/roots/fbb2191bc63ac8ff27f29ee542bf779ce0c298a1-0.der b/roots/fbb2191bc63ac8ff27f29ee542bf779ce0c298a1-0.der new file mode 100644 index 0000000..3cd7112 Binary files /dev/null and b/roots/fbb2191bc63ac8ff27f29ee542bf779ce0c298a1-0.der differ diff --git a/roots/fcc3e143973af986f6b33251f56e17bbee86f58c-0.der b/roots/fcc3e143973af986f6b33251f56e17bbee86f58c-0.der new file mode 100644 index 0000000..bcafb43 Binary files /dev/null and b/roots/fcc3e143973af986f6b33251f56e17bbee86f58c-0.der differ diff --git a/test/KitTest.java b/test/KitTest.java new file mode 100644 index 0000000..3659a3c --- /dev/null +++ b/test/KitTest.java @@ -0,0 +1,47 @@ +import com.minamp.net.*; +import java.io.*; +import java.util.*; + +public class KitTest { + public static void main(String[] a) throws Exception { + final File dir = new File(a[0]); + CertVerifier.setRootStore(new CertVerifier.RootStore() { + public List find(byte[] subj) { + String h = CertVerifier.sha1Hex(subj); + List out = null; + for (int i = 0; ; i++) { + File f = new File(dir, h + "-" + i + ".der"); + if (!f.exists()) return out; + try { if (out == null) out = new ArrayList(); out.add(java.nio.file.Files.readAllBytes(f.toPath())); } catch (IOException e) { return null; } + } + } + }); + int fail = 0; + String[] urls = {"https://rss.anosanube.ovh/api/greader.php/reader/api/0/token", "https://nxc.anosanube.ovh/status.php", "https://nxc.anosanube.ovh/status.php", "https://abs.anosanube.ovh/ping"}; + for (String u : urls) { + long t0 = System.currentTimeMillis(); + try { + Http.Response r = Http.get(u, null, 20000); + String b = r.readString(); + System.out.println("ok " + r.code + " " + u + " " + (System.currentTimeMillis() - t0) + "ms " + b.length() + "b"); + } catch (IOException e) { fail++; System.out.println("FAIL " + u + ": " + e); } + } + // wrong pin -> Untrusted(changed) + CertVerifier.setPin("00:11"); + try { Http.get("https://nxc.anosanube.ovh/status.php", null, 20000).readAll(); fail++; System.out.println("FAIL pin mismatch accepted"); } + catch (CertVerifier.Untrusted u) { System.out.println("ok pin mismatch -> Untrusted changed=" + u.changed + " fp=" + u.fingerprint.substring(0, 11) + " subj=" + u.subject + " exp=" + u.expires); + CertVerifier.setPin(u.fingerprint); + Http.Response r = Http.get("https://nxc.anosanube.ovh/status.php", null, 20000); r.readAll(); + System.out.println("ok pinned leaf accepted: " + r.code); + } + CertVerifier.setPin(""); + // self-signed -> Untrusted(!changed) + try { Http.get("https://self-signed.badssl.com/", null, 20000).readAll(); fail++; System.out.println("FAIL self-signed accepted"); } + catch (CertVerifier.Untrusted u) { System.out.println("ok self-signed -> Untrusted changed=" + u.changed); } + catch (IOException e) { System.out.println("?? self-signed: " + e); } + try { Http.get("https://wrong.host.badssl.com/", null, 20000).readAll(); fail++; System.out.println("FAIL wrong host accepted"); } + catch (IOException e) { System.out.println("ok wrong host: " + e.getMessage()); } + System.out.println(DiagLog.dump()); + System.out.println(fail == 0 ? "ALL OK" : fail + " FAILED"); + } +}