/******************************************************************************* * Copyright (c) MOBAC developers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.mapsources.loader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLClassLoader; import java.security.CodeSigner; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import javax.swing.JOptionPane; import mobac.exceptions.MapSourceCreateException; import mobac.exceptions.UnrecoverableDownloadException; import mobac.exceptions.UpdateFailedException; import mobac.mapsources.MapSourcesManager; import mobac.program.Logging; import mobac.program.ProgramInfo; import mobac.program.interfaces.MapSource; import mobac.program.model.MapSourceLoaderInfo; import mobac.program.model.MapSourceLoaderInfo.LoaderType; import mobac.program.model.Settings; import mobac.utilities.GUIExceptionHandler; import mobac.utilities.I18nUtils; import mobac.utilities.Utilities; import mobac.utilities.file.FileExtFilter; import org.apache.commons.codec.binary.Hex; import org.apache.log4j.Level; import org.apache.log4j.Logger; public class MapPackManager { private final Logger log = Logger.getLogger(MapPackManager.class); private final int requiredMapPackVersion; private final File mapPackDir; private final X509Certificate mapPackCert; public MapPackManager(File mapPackDir) throws CertificateException, IOException { this.mapPackDir = mapPackDir; requiredMapPackVersion = Integer.parseInt(System.getProperty("mobac.mappackversion")); CertificateFactory cf = CertificateFactory.getInstance("X.509"); Collection<? extends Certificate> certs = cf.generateCertificates(Utilities .loadResourceAsStream("cert/MapPack.cer")); mapPackCert = (X509Certificate) certs.iterator().next(); } /** * Searches for updated map packs, verifies the signature * * @throws IOException */ public void installUpdates() throws IOException { File[] newMapPacks = mapPackDir.listFiles(new FileExtFilter(".jar.new")); if (newMapPacks == null) throw new IOException("Failed to enumerate installable mappacks"); for (File newMapPack : newMapPacks) { try { testMapPack(newMapPack); String name = newMapPack.getName(); name = name.substring(0, name.length() - 4); // remove ".new" File oldMapPack = new File(mapPackDir, name); if (oldMapPack.isFile()) { // TODO: Check if new map pack file is still compatible // TODO: Check if the downloaded version is newer File oldMapPack2 = new File(mapPackDir, name + ".old"); Utilities.renameFile(oldMapPack, oldMapPack2); } if (!newMapPack.renameTo(oldMapPack)) throw new IOException("Failed to rename file: " + newMapPack); } catch (CertificateException e) { Utilities.deleteFile(newMapPack); log.error("Map pack certificate cerificateion failed (" + newMapPack.getName() + ") installation aborted and file was deleted"); } } } public File[] getAllMapPackFiles() { return mapPackDir.listFiles(new FileExtFilter(".jar")); } public void loadMapPacks(MapSourcesManager mapSourcesManager) throws IOException, CertificateException { File[] mapPacks = getAllMapPackFiles(); for (File mapPackFile : mapPacks) { File oldMapPackFile = new File(mapPackFile.getAbsolutePath() + ".old"); try { loadMapPack(mapPackFile, mapSourcesManager); if (oldMapPackFile.isFile()) Utilities.deleteFile(oldMapPackFile); } catch (MapSourceCreateException e) { if (oldMapPackFile.isFile()) { mapPackFile.deleteOnExit(); File newMapPackFile = new File(mapPackFile.getAbsolutePath() + ".new"); Utilities.renameFile(oldMapPackFile, newMapPackFile); try { JOptionPane.showMessageDialog(null, I18nUtils.localizedStringForKey("msg_update_map_pack_error"), I18nUtils.localizedStringForKey("msg_update_map_pack_error_title"), JOptionPane.INFORMATION_MESSAGE); System.exit(1); } catch (Exception e1) { log.error(e1.getMessage(), e1); } } GUIExceptionHandler.processException(e); } catch (CertificateException e) { throw e; } catch (Exception e) { throw new IOException("Failed to load map pack: " + mapPackFile, e); } } } public void loadMapPack(File mapPackFile, MapSourcesManager mapSourcesManager) throws CertificateException, IOException, MapSourceCreateException { // testMapPack(mapPackFile); URLClassLoader urlCl; URL url = mapPackFile.toURI().toURL(); urlCl = new MapPackClassLoader(url, ClassLoader.getSystemClassLoader()); InputStream manifestIn = urlCl.getResourceAsStream("META-INF/MANIFEST.MF"); String rev = null; if (manifestIn != null) { Manifest mf = new Manifest(manifestIn); rev = mf.getMainAttributes().getValue("MapPackRevision"); manifestIn.close(); if (rev != null) { if ("exported".equals(rev)) { rev = ProgramInfo.getRevisionStr(); } else { rev = Integer.toString(Utilities.parseSVNRevision(rev)); } } mf = null; } MapSourceLoaderInfo loaderInfo = new MapSourceLoaderInfo(LoaderType.MAPPACK, mapPackFile, rev); final Iterator<MapSource> iterator = ServiceLoader.load(MapSource.class, urlCl).iterator(); while (iterator.hasNext()) { try { MapSource ms = iterator.next(); ms.setLoaderInfo(loaderInfo); mapSourcesManager.addMapSource(ms); log.trace("Loaded map source: " + ms.toString() + " (name: " + ms.getName() + ")"); } catch (Error e) { urlCl = null; throw new MapSourceCreateException("Failed to load a map sources from map pack: " + mapPackFile.getName() + " " + e.getMessage(), e); } } } public String downloadMD5SumList() throws IOException, UpdateFailedException { String md5eTag = Settings.getInstance().mapSourcesUpdate.etag; log.debug("Last md5 eTag: " + md5eTag); String updateUrl = System.getProperty("mobac.updateurl"); if (updateUrl == null) throw new RuntimeException("Update url not present"); byte[] data = null; // Proxy p = new Proxy(Type.HTTP, InetSocketAddress.createUnresolved("localhost", 8888)); HttpURLConnection conn = (HttpURLConnection) new URL(updateUrl).openConnection(); conn.setInstanceFollowRedirects(false); if (md5eTag != null) conn.addRequestProperty("If-None-Match", md5eTag); int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { log.debug("No newer md5 file available"); return null; } if (responseCode != HttpURLConnection.HTTP_OK) throw new UpdateFailedException("Invalid HTTP response: " + responseCode + " for update url " + conn.getURL()); // Case HTTP_OK InputStream in = conn.getInputStream(); data = Utilities.getInputBytes(in); in.close(); Settings.getInstance().mapSourcesUpdate.etag = conn.getHeaderField("ETag"); log.debug("New md5 file retrieved"); String md5sumList = new String(data); return md5sumList; } /** * Clean up old files (<code>.jar.new</code> and <code>jar.unverified</code>)in mapsources directory * * @throws IOException */ public void cleanMapPackDir() throws IOException { File[] newMapPacks = mapPackDir.listFiles(new FileExtFilter(".jar.new")); for (File newMapPack : newMapPacks) Utilities.deleteFile(newMapPack); File[] unverifiedMapPacks = mapPackDir.listFiles(new FileExtFilter(".jar.unverified")); for (File unverifiedMapPack : unverifiedMapPacks) Utilities.deleteFile(unverifiedMapPack); } /** * Performs on map sources online update * * @return <ul> * <li>0: no change in online md5 sum file (based on ETag)</li> * <li>-1: Online md5 file is empty indicationg that this MOBAc versiosn is no longer supported</li> * <li>x>0: Number of updated map packs</li> * </ul> * @throws IOException */ public int updateMapPacks() throws UpdateFailedException, UnrecoverableDownloadException, IOException { String updateBaseUrl = System.getProperty("mobac.updatebaseurl"); if (updateBaseUrl == null) throw new RuntimeException("Update base url not present"); cleanMapPackDir(); String md5sumList = downloadMD5SumList(); if (md5sumList == null) return 0; // no new md5 file available if (md5sumList.length() == 0) return -1; // empty file means - outdated version int updateCount = 0; String[] outdatedMapPacks = searchForOutdatedMapPacks(md5sumList); for (String mapPack : outdatedMapPacks) { log.debug("Updaing map pack: " + mapPack); try { File newMapPackFile = downloadMapPack(updateBaseUrl, mapPack); try { testMapPack(newMapPackFile); } catch (CertificateException e) { // Certificate validation failed log.error(e.getMessage(), e); Utilities.deleteFile(newMapPackFile); continue; } log.debug("Verification of map pack \"" + mapPack + "\" passed successfully"); // Check if the downloaded version is newer int newRev = getMapPackRevision(newMapPackFile); File oldMapPack = new File(mapPackDir, mapPack); int oldRev = -1; if (oldMapPack.isFile()) oldRev = getMapPackRevision(oldMapPack); if (newRev < oldRev) { log.warn("Downloaded map pack was older than existing map pack - ignoring update"); Utilities.deleteFile(newMapPackFile); } else { String name = newMapPackFile.getName(); name = name.replace(".unverified", ".new"); File f = new File(newMapPackFile.getParentFile(), name); // Change file extension Utilities.renameFile(newMapPackFile, f); updateCount++; } } catch (IOException e) { log.error(e.getMessage(), e); } } return updateCount; } public int getMapPackRevision(File mapPackFile) throws ZipException, IOException { ZipFile zip = new ZipFile(mapPackFile); try { ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF"); if (entry == null) throw new ZipException("Unable to find MANIFEST.MF"); Manifest mf = new Manifest(zip.getInputStream(entry)); Attributes a = mf.getMainAttributes(); String mpv = a.getValue("MapPackRevision").trim(); return Utilities.parseSVNRevision(mpv); } catch (NumberFormatException e) { return -1; } finally { zip.close(); } } public File downloadMapPack(String baseURL, String mapPackFilename) throws IOException { if (!mapPackFilename.endsWith(".jar")) throw new IOException("Invalid map pack filename"); byte[] mapPackData = Utilities.downloadHttpFile(baseURL + mapPackFilename); File newMapPackFile = new File(mapPackDir, mapPackFilename + ".unverified"); FileOutputStream out = new FileOutputStream(newMapPackFile); try { out.write(mapPackData); out.flush(); } finally { Utilities.closeStream(out); } log.debug("New map pack \"" + mapPackFilename + "\" successfully downloaded"); return newMapPackFile; } /** * * @param md5sumList * @return Array of filenames of map packs which are outdated */ public String[] searchForOutdatedMapPacks(String md5sumList) throws UpdateFailedException { ArrayList<String> outdatedMappacks = new ArrayList<String>(); String[] md5s = md5sumList.split("[\\n\\r]+"); Pattern linePattern = Pattern.compile("([0-9a-f]{32}) (mp-[\\w]+\\.jar)"); for (String line : md5s) { line = line.trim(); if (line.length() == 0) continue; Matcher m = linePattern.matcher(line); if (!m.matches()) { throw new UpdateFailedException("Invalid content found in md5 list: \"" + line + "\""); } String md5 = m.group(1); String filename = m.group(2); // Check if there is already an update map pack File mapPackFile = new File(mapPackDir, filename + ".new"); if (!mapPackFile.isFile()) mapPackFile = new File(mapPackDir, filename); if (!mapPackFile.isFile()) { outdatedMappacks.add(filename); log.debug("local map pack file missing: " + filename); continue; } try { String localmd5 = generateMappackMD5(mapPackFile); if (localmd5.equals(md5)) continue; // No change in map pack log.debug("Found outdated map pack: \"" + filename + "\" local md5: " + localmd5 + " remote md5: " + md5); outdatedMappacks.add(filename); } catch (Exception e) { log.error("Failed to generate md5sum of " + mapPackFile, e); } } String[] result = new String[outdatedMappacks.size()]; outdatedMappacks.toArray(result); return result; } /** * Calculate the md5sum on all files in the map pack file (except those in META-INF) and their filenames inclusive * path in the map pack file). * * @param mapPackFile * @return * @throws IOException * @throws NoSuchAlgorithmException */ public String generateMappackMD5(File mapPackFile) throws IOException, NoSuchAlgorithmException { ZipFile zip = new ZipFile(mapPackFile); try { Enumeration<? extends ZipEntry> entries = zip.entries(); MessageDigest md5Total = MessageDigest.getInstance("MD5"); MessageDigest md5 = MessageDigest.getInstance("MD5"); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (entry.isDirectory()) continue; // Do not hash files from META-INF String name = entry.getName(); if (name.toUpperCase().startsWith("META-INF")) continue; md5.reset(); InputStream in = zip.getInputStream(entry); byte[] data = Utilities.getInputBytes(in); in.close(); // name = name.replaceAll("\\\\", "/"); byte[] digest = md5.digest(data); log.trace("Hashsum " + Hex.encodeHexString(digest) + " includes \"" + name + "\""); md5Total.update(digest); md5Total.update(name.getBytes()); } String md5sum = Hex.encodeHexString(md5Total.digest()); log.trace("md5sum of " + mapPackFile.getName() + ": " + md5sum); return md5sum; } finally { zip.close(); } } /** * Verifies the class file signatures of the specified map pack * * @param mapPackFile * @throws IOException * @throws CertificateException */ public void testMapPack(File mapPackFile) throws IOException, CertificateException { String fileName = mapPackFile.getName(); JarFile jf = new JarFile(mapPackFile, true); try { Enumeration<JarEntry> it = jf.entries(); while (it.hasMoreElements()) { JarEntry entry = it.nextElement(); // We verify only class files if (!entry.getName().endsWith(".class")) continue; // directory or other entry // Get the input stream (triggers) the signature verification for the specific class Utilities.readFully(jf.getInputStream(entry)); if (entry.getCodeSigners() == null) throw new CertificateException("Unsigned class file found: " + entry.getName()); CodeSigner signer = entry.getCodeSigners()[0]; List<? extends Certificate> cp = signer.getSignerCertPath().getCertificates(); if (cp.size() > 1) throw new CertificateException("Signature certificate not accepted: " + "certificate path contains more than one certificate"); // Compare the used certificate with the mapPack certificate if (!mapPackCert.equals(cp.get(0))) throw new CertificateException("Signature certificate not accepted: " + "not the MapPack signer certificate"); } Manifest mf = jf.getManifest(); Attributes a = mf.getMainAttributes(); String mpv = a.getValue("MapPackVersion"); if (mpv == null) throw new IOException("MapPackVersion info missing!"); int mapPackVersion = Integer.parseInt(mpv); if (requiredMapPackVersion != mapPackVersion) throw new IOException("This pack \"" + fileName + "\" is not compatible with this MOBAC version."); ZipEntry entry = jf.getEntry("META-INF/services/mobac.program.interfaces.MapSource"); if (entry == null) throw new IOException("MapSources services list is missing in file " + fileName); } finally { jf.close(); } } public static void main(String[] args) { try { Logging.configureConsoleLogging(Level.DEBUG); ProgramInfo.initialize(); MapPackManager mpm = new MapPackManager(new File("mapsources")); // System.out.println(mpm.generateMappackMD5(new File("mapsources/mp-bing.jar"))); mpm.updateMapPacks(); } catch (Exception e) { e.printStackTrace(); } } }