iter)
+ {
+ while (iter.hasNext()) {
+ iter.next().clearMarker();
+ }
+ }
+
+ /**
+ * Downloads a new copy of CONFIG_FILE.
+ */
+ protected void downloadConfigFile ()
+ throws IOException
+ {
+ downloadControlFile(CONFIG_FILE, 0);
+ }
+
+ /**
+ * @return true if gettingdown.lock was unlocked, already locked by this application or if
+ * we're not locking at all.
+ */
+ public synchronized boolean lockForUpdates ()
+ {
+ if (_lock != null && _lock.isValid()) {
+ return true;
+ }
+ try {
+ _lockChannel = new RandomAccessFile(getLocalPath("gettingdown.lock"), "rw").getChannel();
+ } catch (FileNotFoundException e) {
+ log.warning("Unable to create lock file", "message", e.getMessage(), e);
+ return false;
+ }
+ try {
+ _lock = _lockChannel.tryLock();
+ } catch (IOException e) {
+ log.warning("Unable to create lock", "message", e.getMessage(), e);
+ return false;
+ } catch (OverlappingFileLockException e) {
+ log.warning("The lock is held elsewhere in this JVM", e);
+ return false;
+ }
+ log.info("Able to lock for updates: " + (_lock != null));
+ return _lock != null;
+ }
+
+ /**
+ * Release gettingdown.lock
+ */
+ public synchronized void releaseLock ()
+ {
+ if (_lock != null) {
+ log.info("Releasing lock");
+ try {
+ _lock.release();
+ } catch (IOException e) {
+ log.warning("Unable to release lock", "message", e.getMessage(), e);
+ }
+ try {
+ _lockChannel.close();
+ } catch (IOException e) {
+ log.warning("Unable to close lock channel", "message", e.getMessage(), e);
+ }
+ _lockChannel = null;
+ _lock = null;
+ }
+ }
+
+ /**
+ * Downloads the digest files and validates their signature.
+ * @throws IOException
+ */
+ protected void downloadDigestFiles ()
+ throws IOException
+ {
+ for (int version = 1; version <= Digest.VERSION; version++) {
+ downloadControlFile(Digest.digestFile(version), version);
+ }
+ }
+
+ /**
+ * Downloads a new copy of the specified control file, optionally validating its signature.
+ * If the download is successful, moves it over the old file on the filesystem.
+ *
+ * TODO: Switch to PKCS #7 or CMS.
+ *
+ * @param sigVersion if {@code 0} no validation will be performed, if {@code > 0} then this
+ * should indicate the version of the digest file being validated which indicates which
+ * algorithm to use to verify the signature. See {@link Digest#VERSION}.
+ */
+ protected void downloadControlFile (String path, int sigVersion)
+ throws IOException
+ {
+ File target = downloadFile(path);
+
+ if (sigVersion > 0) {
+ if (_envc.certs.isEmpty()) {
+ log.info("No signing certs, not verifying digest.txt", "path", path);
+
+ } else {
+ File signatureFile = downloadFile(path + SIGNATURE_SUFFIX);
+ byte[] signature = null;
+ try (FileInputStream signatureStream = new FileInputStream(signatureFile)) {
+ signature = StreamUtil.toByteArray(signatureStream);
+ } finally {
+ FileUtil.deleteHarder(signatureFile); // delete the file regardless
+ }
+
+ byte[] buffer = new byte[8192];
+ int length, validated = 0;
+ for (Certificate cert : _envc.certs) {
+ try (FileInputStream dataInput = new FileInputStream(target)) {
+ Signature sig = Signature.getInstance(Digest.sigAlgorithm(sigVersion));
+ sig.initVerify(cert);
+ while ((length = dataInput.read(buffer)) != -1) {
+ sig.update(buffer, 0, length);
+ }
+
+ if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
+ log.info("Signature does not match", "cert", cert.getPublicKey());
+ continue;
+ } else {
+ log.info("Signature matches", "cert", cert.getPublicKey());
+ validated++;
+ }
+
+ } catch (IOException ioe) {
+ log.warning("Failure validating signature of " + target + ": " + ioe);
+
+ } catch (GeneralSecurityException gse) {
+ // no problem!
+
+ }
+ }
+
+ // if we couldn't find a key that validates our digest, we are the hosed!
+ if (validated == 0) {
+ // delete the temporary digest file as we know it is invalid
+ FileUtil.deleteHarder(target);
+ throw new IOException("m.corrupt_digest_signature_error");
+ }
+ }
+ }
+
+ // now move the temporary file over the original
+ File original = getLocalPath(path);
+ if (!FileUtil.renameTo(target, original)) {
+ throw new IOException("Failed to rename(" + target + ", " + original + ")");
+ }
+ }
+
+ /**
+ * Download a path to a temporary file, returning a {@link File} instance with the path
+ * contents.
+ */
+ protected File downloadFile (String path)
+ throws IOException
+ {
+ File target = getLocalPath(path + "_new");
+
+ URL targetURL = null;
+ try {
+ targetURL = getRemoteURL(path);
+ } catch (Exception e) {
+ log.warning("Requested to download invalid control file",
+ "appbase", _vappbase, "path", path, "error", e);
+ throw (IOException) new IOException("Invalid path '" + path + "'.").initCause(e);
+ }
+
+ log.info("Attempting to refetch '" + path + "' from '" + targetURL + "'.");
+
+ // stream the URL into our temporary file
+ URLConnection uconn = ConnectionUtil.open(proxy, targetURL, 0, 0);
+ // we have to tell Java not to use caches here, otherwise it will cache any request for
+ // same URL for the lifetime of this JVM (based on the URL string, not the URL object);
+ // if the getdown.txt file, for example, changes in the meanwhile, we would never hear
+ // about it; turning off caches is not a performance concern, because when Getdown asks
+ // to download a file, it expects it to come over the wire, not from a cache
+ uconn.setUseCaches(false);
+ uconn.setRequestProperty("Accept-Encoding", "gzip");
+ try (InputStream fin = uconn.getInputStream()) {
+ String encoding = uconn.getContentEncoding();
+ boolean gzip = "gzip".equalsIgnoreCase(encoding);
+ try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) {
+ try (FileOutputStream fout = new FileOutputStream(target)) {
+ StreamUtil.copy(fin2, fout);
+ }
+ }
+ }
+
+ return target;
+ }
+
+ /** Helper function for creating {@link Resource} instances. */
+ protected Resource createResource (String path, EnumSet attrs)
+ throws MalformedURLException
+ {
+ return new Resource(path, getRemoteURL(path), getLocalPath(path), attrs);
+ }
+
+ /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */
+ protected static void addAll (String[] values, List target) {
+ if (values != null) {
+ for (String value : values) {
+ target.add(value);
+ }
+ }
+ }
+
+ /**
+ * Make an immutable List from the specified int array.
+ */
+ public static List intsToList (int[] values)
+ {
+ List list = new ArrayList<>(values.length);
+ for (int val : values) {
+ list.add(val);
+ }
+ return Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Make an immutable List from the specified String array.
+ */
+ public static List stringsToList (String[] values)
+ {
+ return values == null ? null : Collections.unmodifiableList(Arrays.asList(values));
+ }
+
+ /** Used to parse resources with the specified name. */
+ protected void parseResources (Config config, String name, EnumSet attrs,
+ List list)
+ {
+ String[] rsrcs = config.getMultiValue(name);
+ if (rsrcs == null) {
+ return;
+ }
+ for (String rsrc : rsrcs) {
+ try {
+ list.add(createResource(rsrc, attrs));
+ } catch (Exception e) {
+ log.warning("Invalid resource '" + rsrc + "'. " + e);
+ }
+ }
+ }
+
+ /** Possibly generates and returns a google analytics tracking cookie. */
+ protected String getGATrackingCode ()
+ {
+ if (_trackingGAHash == null) {
+ return "";
+ }
+ long time = System.currentTimeMillis() / 1000;
+ if (_trackingStart == 0) {
+ _trackingStart = time;
+ }
+ if (_trackingId == 0) {
+ int low = 100000000, high = 1000000000;
+ _trackingId = low + _rando.nextInt(high-low);
+ }
+ StringBuilder cookie = new StringBuilder("&utmcc=__utma%3D").append(_trackingGAHash);
+ cookie.append(".").append(_trackingId);
+ cookie.append(".").append(_trackingStart).append(".").append(_trackingStart);
+ cookie.append(".").append(time).append(".1%3B%2B");
+ cookie.append("__utmz%3D").append(_trackingGAHash).append(".");
+ cookie.append(_trackingStart).append(".1.1.");
+ cookie.append("utmcsr%3D(direct)%7Cutmccn%3D(direct)%7Cutmcmd%3D(none)%3B");
+ int low = 1000000000, high = 2000000000;
+ cookie.append("&utmn=").append(_rando.nextInt(high-low));
+ return cookie.toString();
+ }
+
+ /**
+ * Encodes a path for use in a URL.
+ */
+ protected static String encodePath (String path)
+ {
+ try {
+ // we want to keep slashes because we're encoding an entire path; also we need to turn
+ // + into %20 because web servers don't like + in paths or file names, blah
+ return URLEncoder.encode(path, "UTF-8").replace("%2F", "/").replace("+", "%20");
+ } catch (UnsupportedEncodingException ue) {
+ log.warning("Failed to URL encode " + path + ": " + ue);
+ return path;
+ }
+ }
+
+ protected File getLocalPath (File appdir, String path)
+ {
+ return new File(appdir, path);
+ }
+
+ public static void setStartupFilesFromParameterString(String p) {
+ // multiple files *might* be passed in as space separated quoted filenames
+ String q = "\"";
+ if (!StringUtil.isBlank(p)) {
+ String[] filenames;
+ // split quoted params or treat as single string array
+ if (p.startsWith(q) && p.endsWith(q)) {
+ // this fails if, e.g.
+ // p=q("stupidfilename\" " "otherfilename")
+ // let's hope no-one ever ends a filename with '" '
+ filenames = p.substring(q.length(),p.length()-q.length()).split(q+" "+q);
+ } else {
+ // single unquoted filename
+ filenames = new String[]{p};
+ }
+
+ // check for locator file. Only allow one locator file to be double clicked (if multiple files opened, ignore locator files)
+ String locatorFilename = filenames.length >= 1 ? filenames[0] : null;
+ if (
+ !StringUtil.isBlank(locatorFilename)
+ && locatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)
+ ) {
+ setLocatorFile(locatorFilename);
+ // remove the locator filename from the filenames array
+ String[] otherFilenames = new String[filenames.length - 1];
+ System.arraycopy(filenames, 1, otherFilenames, 0, otherFilenames.length);
+ filenames = otherFilenames;
+ }
+
+ for (int i = 0; i < filenames.length; i++) {
+ String filename = filenames[i];
+ // skip any other locator files in a multiple file list
+ if (! filename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)) {
+ addStartupFile(filename);
+ }
+ }
+ }
+ }
+
+ public static void setLocatorFile(String filename) {
+ _locatorFile = new File(filename);
+ }
+
+ public static void addStartupFile(String filename) {
+ _startupFiles.add(new File(filename));
+ }
+
+ private Config createLocatorConfig(Config.ParseOpts opts) {
+ if (_locatorFile == null) {
+ return null;
+ }
+
+ Config locatorConfig = null;
+
+ try {
+ Config tmpConfig = null;
+ if (_locatorFile.exists()) {
+ tmpConfig = Config.parseConfig(_locatorFile, opts);
+ } else {
+ log.warning("Given locator file does not exist", "file", _locatorFile);
+ }
+
+ // appbase is sanitised in HostWhitelist
+ Map tmpData = new HashMap<>();
+ for (Map.Entry entry : tmpConfig.getData().entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key;
+ if (Config.allowedReplaceKeys.contains(mkey) || Config.allowedMergeKeys.contains(mkey)) {
+ tmpData.put(key, value);
+ }
+ }
+ locatorConfig = new Config(tmpData);
+
+ } catch (Exception e) {
+ log.warning("Failure reading locator file", "file", _locatorFile, e);
+ }
+
+ log.info("Returning locatorConfig", locatorConfig);
+
+ return locatorConfig;
+ }
+
+ protected final EnvConfig _envc;
+ protected File _config;
+ protected Digest _digest;
+
+ protected long _version = -1;
+ protected long _targetVersion = -1;
+ protected String _appbase;
+ protected URL _vappbase;
+ protected URL _latest;
+ protected String _class;
+ protected String _dockName;
+ protected String _dockIconPath;
+ protected boolean _strictComments;
+ protected boolean _windebug;
+ protected boolean _allowOffline;
+ protected int _maxConcDownloads;
+
+ protected String _trackingURL;
+ protected Set _trackingPcts;
+ protected String _trackingCookieName;
+ protected String _trackingCookieProperty;
+ protected String _trackingURLSuffix;
+ protected String _trackingGAHash;
+ protected long _trackingStart;
+ protected int _trackingId;
+
+ protected String _javaVersionProp = "java.version";
+ protected String _javaVersionRegex = "(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?";
+ protected long _javaMinVersion, _javaMaxVersion;
+ protected boolean _javaExactVersionRequired;
+ protected String _javaLocation;
+
+ protected List _codes = new ArrayList<>();
+ protected List _resources = new ArrayList<>();
+
+ protected boolean _useCodeCache;
+ protected int _codeCacheRetentionDays;
+
+ protected Map _auxgroups = new HashMap<>();
+ protected Map _auxactive = new HashMap<>();
+
+ protected List _jvmargs = new ArrayList<>();
+ protected List _appargs = new ArrayList<>();
+
+ protected String[] _optimumJvmArgs;
+
+ protected List _txtJvmArgs = new ArrayList<>();
+
+ /** If a warning has been issued about not being able to set modtimes. */
+ protected boolean _warnedAboutSetLastModified;
+
+ /** Locks gettingdown.lock in the app dir. Held the entire time updating is going on.*/
+ protected FileLock _lock;
+
+ /** Channel to the file underlying _lock. Kept around solely so the lock doesn't close. */
+ protected FileChannel _lockChannel;
+
+ protected Random _rando = new Random();
+
+ protected static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ protected static final String ENV_VAR_PREFIX = "%ENV.";
+ protected static final Pattern ENV_VAR_PATTERN = Pattern.compile("%ENV\\.(.*?)%");
+
+ protected static File _locatorFile;
+ protected static List _startupFiles = new ArrayList<>();
+ public static final String LOCATOR_FILE_EXTENSION = "jvl";
+}