334 lines
11 KiB
Java
334 lines
11 KiB
Java
package com.derpfish.pinkielive.download;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URL;
|
|
import java.net.URLConnection;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.KeyFactory;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.PublicKey;
|
|
import java.security.Signature;
|
|
import java.security.SignatureException;
|
|
import java.security.spec.InvalidKeySpecException;
|
|
import java.security.spec.X509EncodedKeySpec;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Properties;
|
|
import java.util.zip.ZipEntry;
|
|
import java.util.zip.ZipInputStream;
|
|
|
|
import javax.xml.parsers.DocumentBuilder;
|
|
import javax.xml.parsers.DocumentBuilderFactory;
|
|
import javax.xml.parsers.ParserConfigurationException;
|
|
|
|
import org.w3c.dom.Document;
|
|
import org.w3c.dom.Element;
|
|
import org.w3c.dom.Node;
|
|
import org.xml.sax.SAXException;
|
|
|
|
import com.derpfish.pinkielive.animation.PonyAnimation;
|
|
import com.derpfish.pinkielive.util.Base64;
|
|
import com.derpfish.pinkielive.util.IOUtils;
|
|
|
|
import dalvik.system.DexClassLoader;
|
|
|
|
public class PonyDownloader
|
|
{
|
|
private static final String animationsUrl = "http://animations.pinkie-live.googlecode.com/git/animations.xml";
|
|
private static final String animationsSigUrl = "http://animations.pinkie-live.googlecode.com/git/animations.sig";
|
|
|
|
private static final String PUBLIC_KEY_ENCODED =
|
|
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1aP3qdJO5F64+UQVF1zl" +
|
|
"DyyM/bN/jJZgU9guq7RLLO81y1IFpxL0Rs66FMkcSz4ZtdtOT6uRhZ9TvrVhMR69" +
|
|
"WpxdH3hrh4JhILd+/ZRxQt2GX4FyLDIPqfpA867ZjS+lrgncN48kC2X3z3ETQ46Q" +
|
|
"eCjYrLfKeseGy620dWuIV2yXenr1NHSJ5kwOKvOdddEHwijSNwpDo8C93XGCAHtT" +
|
|
"fXcmknBeVPNWC1iL0CshpMWuDIvLEh867J6HvpSzyAss0q62mvRyttifjZO8aiSH" +
|
|
"LctTLxMnTrxhL9mw4lmFCzI0UoWmeSOiEOQTXhGxWhVE9gXv0jzizTvX5DG9MtTA" +
|
|
"CwIDAQAB";
|
|
|
|
private static Object lock = new Object();
|
|
private static PublicKey publicKey = null;
|
|
|
|
public static boolean verifyData(final InputStream istream, final String base64sig)
|
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException, InvalidKeySpecException
|
|
{
|
|
synchronized(lock)
|
|
{
|
|
if (publicKey == null)
|
|
{
|
|
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(PUBLIC_KEY_ENCODED, Base64.DEFAULT));
|
|
final KeyFactory kf = KeyFactory.getInstance("RSA");
|
|
publicKey = kf.generatePublic(keySpec);
|
|
}
|
|
}
|
|
|
|
final Signature signature = Signature.getInstance("SHA256withRSA");
|
|
signature.initVerify(publicKey);
|
|
|
|
final byte[] buffer = new byte[4096];
|
|
int nread = 0;
|
|
while ((nread = istream.read(buffer)) >= 0)
|
|
{
|
|
signature.update(buffer, 0, nread);
|
|
}
|
|
istream.close();
|
|
|
|
return signature.verify(Base64.decode(base64sig, Base64.DEFAULT));
|
|
}
|
|
|
|
public static List<PonyAnimationListing> fetchListings() throws ParserConfigurationException, SAXException, IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException
|
|
{
|
|
final byte[] xmlBytes = fetchUrl(animationsUrl, 131072);
|
|
final byte[] sigBytes = fetchUrl(animationsSigUrl, 4096);
|
|
if (!verifyData(new ByteArrayInputStream(xmlBytes), new String(sigBytes)))
|
|
{
|
|
throw new IllegalStateException("Unable to verify signature of animations.xml");
|
|
}
|
|
|
|
return parseListings(new ByteArrayInputStream(xmlBytes));
|
|
}
|
|
|
|
private static byte[] fetchUrl(final String url, int maxSize) throws MalformedURLException, IOException
|
|
{
|
|
final URLConnection connection = new URL(url).openConnection();
|
|
connection.connect();
|
|
|
|
final InputStream istream = connection.getInputStream();
|
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
int totalRead = 0;
|
|
int nRead = 0;
|
|
final byte[] buffer = new byte[4096];
|
|
while ((nRead = istream.read(buffer)) >= 0)
|
|
{
|
|
baos.write(buffer, 0, nRead);
|
|
totalRead += nRead;
|
|
if (maxSize > 0 && totalRead > maxSize)
|
|
{
|
|
throw new IllegalStateException("Retrieved animations.xml exceeds maximum allowed size.");
|
|
}
|
|
}
|
|
istream.close();
|
|
baos.close();
|
|
|
|
return baos.toByteArray();
|
|
}
|
|
|
|
private static List<PonyAnimationListing> parseListings(final InputStream istream) throws ParserConfigurationException, SAXException, IOException
|
|
{
|
|
final List<PonyAnimationListing> animationListings = new ArrayList<PonyAnimationListing>();
|
|
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|
final DocumentBuilder builder = factory.newDocumentBuilder();
|
|
final Document dom = builder.parse(istream);
|
|
istream.close();
|
|
|
|
final Element root = dom.getDocumentElement();
|
|
if (!root.getNodeName().equals("animations"))
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
|
|
for (int i = 0; i < root.getChildNodes().getLength(); i++)
|
|
{
|
|
final Node animation = root.getChildNodes().item(i);
|
|
if (animation.getNodeType() == Node.TEXT_NODE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!animation.getNodeName().equals("animation"))
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
|
|
final PonyAnimationListing animListing = new PonyAnimationListing();
|
|
for (int j = 0; j < animation.getChildNodes().getLength(); j++)
|
|
{
|
|
final Node attr = animation.getChildNodes().item(j);
|
|
if (attr.getNodeType() == Node.TEXT_NODE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (attr.getNodeName().equals("name"))
|
|
{
|
|
if (animListing.getName() != null)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
animListing.setName(getNodeText(attr));
|
|
}
|
|
else if (attr.getNodeName().equals("url"))
|
|
{
|
|
if (animListing.getUrl() != null)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
animListing.setUrl(getNodeText(attr));
|
|
}
|
|
else if (attr.getNodeName().equals("id"))
|
|
{
|
|
if (animListing.getId() != null)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
animListing.setId(getNodeText(attr));
|
|
}
|
|
else if (attr.getNodeName().equals("version"))
|
|
{
|
|
if (animListing.getVersion() != null)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
animListing.setVersion(Long.parseLong(getNodeText(attr)));
|
|
}
|
|
else if (attr.getNodeName().equals("checksum"))
|
|
{
|
|
if (animListing.getChecksum() != null)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
animListing.setChecksum(getNodeText(attr));
|
|
}
|
|
else
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
}
|
|
|
|
if (animListing.getName() == null || animListing.getUrl() == null
|
|
|| animListing.getId() == null || animListing.getVersion() == null
|
|
|| animListing.getChecksum() == null)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
|
|
animationListings.add(animListing);
|
|
}
|
|
|
|
return animationListings;
|
|
}
|
|
|
|
private static String getNodeText(final Node node)
|
|
{
|
|
if (node.getChildNodes().getLength() != 1)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
final Node innerNode = node.getChildNodes().item(0);
|
|
if (innerNode.getNodeType() != Node.TEXT_NODE)
|
|
{
|
|
throw new IllegalArgumentException("Malformed XML");
|
|
}
|
|
return innerNode.getNodeValue();
|
|
}
|
|
|
|
public static void fetchPony(final File dataDir, final File cacheDir, final PonyAnimationListing animation) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException
|
|
{
|
|
final File ponyDir = new File(dataDir.getAbsolutePath() + File.separator + "ponies");
|
|
if (!ponyDir.exists())
|
|
{
|
|
ponyDir.mkdir();
|
|
}
|
|
final File tmpFile = new File(cacheDir.getAbsolutePath() + File.separator + "delme.zip");
|
|
|
|
final URLConnection connection = new URL(animation.getUrl()).openConnection();
|
|
connection.connect();
|
|
IOUtils.copyStreamAndClose(connection.getInputStream(), new FileOutputStream(tmpFile));
|
|
|
|
if (!verifyData(new FileInputStream(tmpFile), animation.getChecksum()))
|
|
{
|
|
throw new IllegalStateException("Signature verification failed.");
|
|
}
|
|
|
|
final File animDir = new File(ponyDir.getAbsolutePath() + File.separator + animation.getId());
|
|
if (animDir.exists())
|
|
{
|
|
for (File file : animDir.listFiles())
|
|
{
|
|
file.delete();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
animDir.mkdir();
|
|
}
|
|
|
|
final InputStream istr = new FileInputStream(tmpFile);
|
|
final ZipInputStream zis = new ZipInputStream(istr);
|
|
ZipEntry zipEntry = null;
|
|
while ((zipEntry = zis.getNextEntry()) != null)
|
|
{
|
|
final FileOutputStream fos = new FileOutputStream(animDir.getAbsolutePath() + File.separator + zipEntry.getName());
|
|
IOUtils.copyStream(zis, fos);
|
|
fos.close();
|
|
}
|
|
zis.close();
|
|
istr.close();
|
|
|
|
tmpFile.delete();
|
|
}
|
|
|
|
public static List<PonyAnimationContainer> getPonyAnimations(final File dataDir, final File cacheDir,
|
|
final boolean loadAnimations)
|
|
throws FileNotFoundException, IOException, ClassNotFoundException, IllegalAccessException, InstantiationException
|
|
{
|
|
final List<PonyAnimationContainer> containers = new ArrayList<PonyAnimationContainer>();
|
|
final File ponyDir = new File(dataDir.getAbsolutePath() + File.separator + "ponies");
|
|
if (!ponyDir.exists())
|
|
{
|
|
return containers;
|
|
}
|
|
|
|
for (final File subDir : ponyDir.listFiles())
|
|
{
|
|
if (subDir.isDirectory())
|
|
{
|
|
final File manifest = new File(subDir.getAbsolutePath() + File.separator + "manifest.properties");
|
|
if (!manifest.exists())
|
|
{
|
|
continue;
|
|
}
|
|
final File lib = new File(subDir.getAbsolutePath() + File.separator + "lib.jar");
|
|
if (!lib.exists())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
final Properties properties = new Properties();
|
|
properties.load(new FileInputStream(manifest));
|
|
|
|
final PonyAnimationContainer container = new PonyAnimationContainer();
|
|
container.setId(properties.getProperty("id"));
|
|
container.setName(properties.getProperty("name"));
|
|
container.setVersion(Long.parseLong(properties.getProperty("version")));
|
|
|
|
if (loadAnimations)
|
|
{
|
|
final File animCacheDir = new File(cacheDir.getAbsolutePath() + File.separator + container.getId());
|
|
if (!animCacheDir.exists())
|
|
{
|
|
animCacheDir.mkdir();
|
|
}
|
|
|
|
final DexClassLoader classLoader = new DexClassLoader(lib.getAbsolutePath(),
|
|
animCacheDir.getAbsolutePath(), null, PonyDownloader.class.getClassLoader());
|
|
Class<?> animClass = classLoader.loadClass(properties.getProperty("className"));
|
|
container.setPonyAnimation(animClass.asSubclass(PonyAnimation.class).newInstance());
|
|
container.getPonyAnimation().setResourceDir(subDir);
|
|
}
|
|
|
|
containers.add(container);
|
|
}
|
|
}
|
|
return containers;
|
|
}
|
|
}
|