# Conflicts:
#	core/src/main/java/com/threerings/getdown/data/Application.java
This commit is contained in:
e507802
2019-05-29 15:44:29 +02:00
43 changed files with 1188 additions and 659 deletions
+13
View File
@@ -5,6 +5,19 @@
* Fixed issues with proxy information not getting properly passed through to app.
Via [#216](//github.com/threerings/getdown/pull/216).
* `appbase` and `latest` properties in `getdown.txt` now process env var subtitutions.
* Added support for [Proxy Auto-config](https://en.wikipedia.org/wiki/Proxy_auto-config) via PAC
files.
* Proxy handling can now recover from credentials going out of date. It will detect the error and
ask for updated credentials.
* Added `try_no_proxy` system property. This instructs Getdown to always first try to run without a
proxy, regardless of whether it has been configured to use a proxy in the past. And if it can run
without a proxy, it does so for that session, but retains the proxy config for future sessions in
which the proxy may again be needed.
## 1.8.4 - May 14, 2019
* Added `verify_timeout` config to allow customization of the default (60 second) timeout during
@@ -7,16 +7,13 @@ package com.threerings.getdown.tools;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import com.threerings.getdown.data.Digest;
/**
* An ant task used to create a <code>digest.txt</code> for a Getdown
* An ant task used to create a {@code digest.txt} for a Getdown
* application deployment.
*/
public class DigesterTask extends Task
@@ -5,16 +5,16 @@
package com.threerings.getdown.tests;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import org.junit.*;
import static org.junit.Assert.*;
import com.threerings.getdown.tools.Digester;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class DigesterIT {
@@ -32,23 +32,23 @@ public class DigesterIT {
Files.delete(digest2);
assertEquals(Arrays.asList(
"getdown.txt = 779c74fb4b251e18faf6e240a0667964",
"getdown.txt = 9c9b2494929c99d44ae51034d59e1a1b",
"testapp.jar = 404dafa55e78b25ec0e3a936357b1883",
"funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249",
"crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16",
"foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec",
"script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf",
"digest.txt = f5561d85e4d80cc85883963897e58ff6"
"digest.txt = 11f9ba349cf9edacac4d72a3158447e5"
), digestLines);
assertEquals(Arrays.asList(
"getdown.txt = 4f0c657895c3c3a35fa55bf5951c64fa9b0694f8fc685af3f1d8635c639e066b",
"getdown.txt = 1efecfae2a189002a6658f17d162b1922c7bde978944949276dc038a0df2461f",
"testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0",
"funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
"crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b",
"foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062",
"script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988",
"digest2.txt = 70b442c9f56660561921da3368e1a206f05c379182fab3062750b7ddcf303407"
"digest2.txt = 41eacdabda8909bdbbf61e4f980867f4003c16a12f6770e6fc619b6af100e05b"
), digest2Lines);
}
}
+12
View File
@@ -13,6 +13,18 @@ apparg = %APPDIR%
# test the %env% mechanism
jvmarg = -Dusername=\%ENV.USER%
# test various java_*** configs, they are not interesting for digester
java_local_dir = jre
java_max_version = 1089999
java_min_version = [windows] 1080111
java_min_version = [!windows] 1080192
java_exact_version_required = [linux] true
java_location = [linux-amd64] /files/java/java_linux_64.zip
java_location = [linux-i386] /files/java/java_linux_32.zip
java_location = [mac] /files/java/java_mac_64.zip
java_location = [windows-amd64] /files/java/java_windows_64.zip
java_location = [windows-x86] /files/java/java_windows_32.zip
strict_comments = true
resource = funny%test dir/some=file.txt
resource = crazyhashfile#txt
@@ -15,7 +15,7 @@ import java.util.logging.*;
/**
* A placeholder class that contains a reference to the log object used by the Getdown code.
*/
public class Log
public final class Log
{
public static class Shim {
/**
@@ -143,6 +143,5 @@ public class Log
protected FieldPosition _fpos = new FieldPosition(SimpleDateFormat.DATE_FIELD);
}
protected static final String DATE_FORMAT = "{0,date} {0,time}";
protected static final Level[] LEVELS = {Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE};
}
@@ -96,6 +96,6 @@ public class GarbageCollector
private static File getCachedFile (File file)
{
return !isLastAccessedFile(file) ? file : new File(
file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf(".")));
file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf('.')));
}
}
@@ -69,7 +69,7 @@ public class ResourceCache
private String getFileSuffix (File fileToCache) {
String fileName = fileToCache.getName();
int index = fileName.lastIndexOf(".");
int index = fileName.lastIndexOf('.');
return index > -1 ? fileName.substring(index) : "";
}
@@ -7,7 +7,6 @@ package com.threerings.getdown.data;
import java.io.*;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
@@ -23,8 +22,8 @@ import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import com.threerings.getdown.net.Connector;
import com.threerings.getdown.util.*;
// avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+
import com.threerings.getdown.util.Base64;
@@ -33,7 +32,7 @@ import static com.threerings.getdown.Log.log;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Parses and provide access to the information contained in the <code>getdown.txt</code>
* Parses and provide access to the information contained in the {@code getdown.txt}
* configuration file.
*/
public class Application
@@ -212,10 +211,10 @@ public class Application
* Used by {@link #verifyMetadata} to communicate status in circumstances where it needs to
* take network actions.
*/
public static interface StatusDisplay
public interface StatusDisplay
{
/** Requests that the specified status message be displayed. */
public void updateStatus (String message);
void updateStatus (String message);
}
/**
@@ -233,19 +232,56 @@ public class Application
}
}
/** The proxy that should be used to do HTTP downloads. This must be configured prior to using
* the application instance. Yes this is a public mutable field, no I'm not going to create a
/**
* Reads the {@code getdown.txt} config file into a {@code Config} object and returns it.
*/
public static Config readConfig (EnvConfig envc, boolean checkPlatform) throws IOException {
Config config = null;
File cfgfile = new File(envc.appDir, CONFIG_FILE);
Config.ParseOpts opts = Config.createOpts(checkPlatform);
try {
// if we have a configuration file, read the data from it
if (cfgfile.exists()) {
config = Config.parseConfig(cfgfile, opts);
}
// otherwise, try reading data from our backup config file; thanks to funny windows
// bullshit, we have to do this backup file fiddling in case we got screwed while
// updating getdown.txt during normal operation
else if ((cfgfile = new File(envc.appDir, Application.CONFIG_FILE + "_old")).exists()) {
config = Config.parseConfig(cfgfile, opts);
}
// otherwise, issue a warning that we found no getdown file
else {
log.info("Found no getdown.txt file", "appdir", envc.appDir);
}
} catch (Exception e) {
log.warning("Failure reading config file", "file", config, e);
}
// if we failed to read our config file, check for an appbase specified via a system
// property; we can use that to bootstrap ourselves back into operation
if (config == null) {
log.info("Using 'appbase' from bootstrap config", "appbase", envc.appBase);
Map<String, Object> cdata = new HashMap<>();
cdata.put("appbase", envc.appBase);
config = new Config(cdata);
}
return config;
}
/** A helper that is used to do HTTP downloads. This must be configured prior to using the
* application instance. Yes this is a public mutable field, no I'm not going to create a
* getter and setter just to pretend like that's not the case. */
public Proxy proxy = Proxy.NO_PROXY;
public Connector conn = Connector.DEFAULT;
/**
* Creates an application instance which records the location of the <code>getdown.txt</code>
* Creates an application instance which records the location of the {@code getdown.txt}
* configuration file from the supplied application directory.
*
*/
public Application (EnvConfig envc) {
_envc = envc;
_config = new File(envc.appDir, CONFIG_FILE);
}
/**
@@ -369,8 +405,7 @@ public class Application
*/
public List<Resource> getActiveCodeResources ()
{
ArrayList<Resource> codes = new ArrayList<>();
codes.addAll(getCodeResources());
List<Resource> codes = new ArrayList<>(getCodeResources());
for (AuxGroup aux : getAuxGroups()) {
if (isAuxGroupActive(aux.name)) {
codes.addAll(aux.codes);
@@ -398,8 +433,7 @@ public class Application
*/
public List<Resource> getActiveResources ()
{
ArrayList<Resource> rsrcs = new ArrayList<>();
rsrcs.addAll(getResources());
List<Resource> rsrcs = new ArrayList<>(getResources());
for (AuxGroup aux : getAuxGroups()) {
if (isAuxGroupActive(aux.name)) {
rsrcs.addAll(aux.rsrcs);
@@ -544,44 +578,22 @@ public class Application
* @exception IOException thrown if there is an error reading the file or an error encountered
* during its parsing.
*/
public Config init (boolean checkPlatform)
throws IOException
public Config init (boolean checkPlatform) throws IOException
{
Config config = null;
File cfgfile = _config;
Config.ParseOpts opts = Config.createOpts(checkPlatform);
try {
// if we have a configuration file, read the data from it
if (cfgfile.exists()) {
config = Config.parseConfig(_config, opts);
}
// otherwise, try reading data from our backup config file; thanks to funny windows
// bullshit, we have to do this backup file fiddling in case we got screwed while
// updating getdown.txt during normal operation
else if ((cfgfile = getLocalPath(CONFIG_FILE + "_old")).exists()) {
config = Config.parseConfig(cfgfile, opts);
}
// otherwise, issue a warning that we found no getdown file
else {
log.info("Found no getdown.txt file", "appdir", getAppDir());
}
} catch (Exception e) {
log.warning("Failure reading config file", "file", config, e);
}
// if we failed to read our config file, check for an appbase specified via a system
// property; we can use that to bootstrap ourselves back into operation
if (config == null) {
String appbase = _envc.appBase;
log.info("Using 'appbase' from bootstrap config", "appbase", appbase);
Map<String, Object> cdata = new HashMap<>();
cdata.put("appbase", appbase);
config = new Config(cdata);
}
// extract our version information
_version = config.getLong("version", -1L);
Config config = readConfig(_envc, checkPlatform);
initBase(config);
initJava(config);
initTracking(config);
initResources(config);
initArgs(config);
return config;
}
/**
* Reads the basic config info from {@code config} into this instance. This includes things
* like the appbase and version.
*/
public void initBase (Config config) throws IOException {
// first determine our application base, this way if anything goes wrong later in the
// process, our caller can use the appbase to download a new configuration file
_appbase = config.getString("appbase");
@@ -589,25 +601,29 @@ public class Application
throw new RuntimeException("m.missing_appbase");
}
// check if we're overriding the domain in the appbase
// check if we're overriding the domain in the appbase, and sub envvars
_appbase = processArg(SysProps.overrideAppbase(_appbase));
// make sure there's a trailing slash
if (!_appbase.endsWith("/")) {
_appbase = _appbase + "/";
_appbase += "/";
}
// extract our version information
_version = config.getLong("version", -1L);
// if we are a versioned deployment, create a versioned appbase
try {
_vappbase = createVAppBase(_version);
} catch (MalformedURLException mue) {
String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
throw (IOException) new IOException(err).initCause(mue);
throw new IOException(err, mue);
}
// check for a latest config URL
String latest = processArg(config.getString("latest"));
String latest = config.getString("latest");
if (latest != null) {
latest = processArg(latest);
if (latest.startsWith(_appbase)) {
latest = _appbase + latest.substring(_appbase.length());
} else {
@@ -620,20 +636,28 @@ public class Application
}
}
String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
// determine our application class name (use app-specific class _if_ one is provided)
_class = config.getString("class");
if (appPrefix.length() > 0) {
_class = config.getString(appPrefix + "class", _class);
}
if (_class == null) {
throw new IOException("m.missing_class");
}
// determine whether we want strict comments
_strictComments = config.getBoolean("strict_comments");
// determine whether we want to allow offline operation (defaults to false)
_allowOffline = config.getBoolean("allow_offline");
// whether to cache code resources and launch from cache
_useCodeCache = config.getBoolean("use_code_cache");
_codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
// maximum simultaneous downloads
_maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads",
SysProps.threadPoolSize()));
_verifyTimeout = config.getInt("verify_timeout", 60);
}
/**
* Reads the JVM requirements from {@code config} into this instance. This includes things like
* the min and max java version, location of a locally installed JRE, etc.
*/
public void initJava (Config config) {
// check to see if we're using a custom java.version property and regex
_javaVersionProp = config.getString("java_version_prop", _javaVersionProp);
_javaVersionRegex = config.getString("java_version_regex", _javaVersionRegex);
@@ -648,17 +672,16 @@ public class Application
// check to see if we require a particular JVM version and have a supplied JVM
_javaExactVersionRequired = config.getBoolean("java_exact_version_required");
// this is a little weird, but when we're run from the digester, we see a String[] which
// contains java locations for all platforms which we can't grok, but the digester doesn't
// need to know about that; when we're run in a real application there will be only one!
Object javaloc = config.getRaw("java_location");
if (javaloc instanceof String) {
_javaLocation = (String)javaloc;
}
_javaLocation = config.getString("java_location");
// used only in conjunction with java_location
_javaLocalDir = getLocalPath(config.getString("java_local_dir", LaunchUtil.LOCAL_JAVA_DIR));
}
/**
* Reads the install tracking info from {@code config} into this instance.
*/
public void initTracking (Config config) {
// determine whether we have any tracking configuration
_trackingURL = config.getString("tracking_url");
@@ -683,14 +706,16 @@ public class Application
// Some app may need to generate google analytics code
_trackingGAHash = config.getString("tracking_ga_hash");
}
/**
* Reads the app resource info from {@code config} into this instance.
*/
public void initResources (Config config) throws IOException {
// clear our arrays as we may be reinitializing
_codes.clear();
_resources.clear();
_auxgroups.clear();
_jvmargs.clear();
_appargs.clear();
_txtJvmArgs.clear();
// parse our code resources
if (config.getMultiValue("code") == null &&
@@ -709,10 +734,10 @@ public class Application
// parse our auxiliary resource groups
for (String auxgroup : config.getList("auxgroups")) {
ArrayList<Resource> codes = new ArrayList<>();
List<Resource> codes = new ArrayList<>();
parseResources(config, auxgroup + ".code", Resource.NORMAL, codes);
parseResources(config, auxgroup + ".ucode", Resource.UNPACK, codes);
ArrayList<Resource> rsrcs = new ArrayList<>();
List<Resource> rsrcs = new ArrayList<>();
parseResources(config, auxgroup + ".resource", Resource.NORMAL, rsrcs);
parseResources(config, auxgroup + ".xresource", Resource.EXEC, rsrcs);
parseResources(config, auxgroup + ".uresource", Resource.UNPACK, rsrcs);
@@ -720,21 +745,38 @@ public class Application
parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs);
_auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs));
}
}
/**
* Reads the command line arg info from {@code config} into this instance.
*/
public void initArgs (Config config) throws IOException {
_jvmargs.clear();
_appargs.clear();
_txtJvmArgs.clear();
String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
// determine our application class name (use app-specific class _if_ one is provided)
_class = config.getString("class");
if (appPrefix.length() > 0) {
_class = config.getString(appPrefix + "class", _class);
}
if (_class == null) {
throw new IOException("m.missing_class");
}
// transfer our JVM arguments (we include both "global" args and app_id-prefixed args)
String[] jvmargs = config.getMultiValue("jvmarg");
addAll(jvmargs, _jvmargs);
addAll(config.getMultiValue("jvmarg"), _jvmargs);
if (appPrefix.length() > 0) {
jvmargs = config.getMultiValue(appPrefix + "jvmarg");
addAll(jvmargs, _jvmargs);
addAll(config.getMultiValue(appPrefix + "jvmarg"), _jvmargs);
}
// get the set of optimum JVM arguments
_optimumJvmArgs = config.getMultiValue("optimum_jvmarg");
// transfer our application arguments
String[] appargs = config.getMultiValue(appPrefix + "apparg");
addAll(appargs, _appargs);
addAll(config.getMultiValue(appPrefix + "apparg"), _appargs);
// add the launch specific application arguments
_appargs.addAll(_envc.appArgs);
@@ -742,27 +784,13 @@ public class Application
// look for custom arguments
fillAssignmentListFromPairs("extra.txt", _txtJvmArgs);
// determine whether we want to allow offline operation (defaults to false)
_allowOffline = config.getBoolean("allow_offline");
// look for a debug.txt file which causes us to run in java.exe on Windows so that we can
// obtain a thread dump of the running JVM
_windebug = getLocalPath("debug.txt").exists();
// whether to cache code resources and launch from cache
_useCodeCache = config.getBoolean("use_code_cache");
_codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
// maximum simultaneous downloads
_maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads",
SysProps.threadPoolSize()));
// extract some info used to configure our child process on macOS
_dockName = config.getString("ui.name");
_dockIconPath = config.getString("ui.mac_dock_icon", "../desktop.icns");
_verifyTimeout = config.getInt("verify_timeout", 60);
return config;
// look for a debug.txt file which causes us to run in java.exe on Windows so that we can
// obtain a thread dump of the running JVM
_windebug = getLocalPath("debug.txt").exists();
}
/**
@@ -791,8 +819,7 @@ public class Application
* Returns a URL from which the specified path can be fetched. Our application base URL is
* properly versioned and combined with the supplied path.
*/
public URL getRemoteURL (String path)
throws MalformedURLException
public URL getRemoteURL (String path) throws MalformedURLException
{
return new URL(_vappbase, encodePath(path));
}
@@ -882,7 +909,7 @@ public class Application
}
/**
* Attempts to redownload the <code>getdown.txt</code> file based on information parsed from a
* Attempts to redownload the {@code getdown.txt} file based on information parsed from a
* previous call to {@link #init}.
*/
public void attemptRecovery (StatusDisplay status)
@@ -893,7 +920,7 @@ public class Application
}
/**
* Downloads and replaces the <code>getdown.txt</code> and <code>digest.txt</code> files with
* Downloads and replaces the {@code getdown.txt} and {@code digest.txt} files with
* those for the target version of our application.
*/
public void updateMetadata ()
@@ -904,7 +931,7 @@ public class Application
_vappbase = createVAppBase(_targetVersion);
} catch (MalformedURLException mue) {
String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
throw (IOException) new IOException(err).initCause(mue);
throw new IOException(err, mue);
}
try {
@@ -961,16 +988,8 @@ public class Application
args.add("-Xdock:name=" + _dockName);
}
// pass along our proxy settings
if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) {
InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address();
String proxyHost = proxyAddr.getHostString();
int proxyPort = proxyAddr.getPort();
args.add("-Dhttp.proxyHost=" + proxyHost);
args.add("-Dhttp.proxyPort=" + proxyPort);
args.add("-Dhttps.proxyHost=" + proxyHost);
args.add("-Dhttps.proxyPort=" + proxyPort);
}
// forward our proxy settings
conn.addProxyArgs(args);
// add the marker indicating the app is running in getdown
args.add("-D" + Properties.GETDOWN + "=true");
@@ -1080,7 +1099,7 @@ public class Application
for (String jvmarg : _jvmargs) {
if (jvmarg.startsWith("-D")) {
jvmarg = processArg(jvmarg.substring(2));
int eqidx = jvmarg.indexOf("=");
int eqidx = jvmarg.indexOf('=');
if (eqidx == -1) {
log.warning("Bogus system property: '" + jvmarg + "'?");
} else {
@@ -1121,10 +1140,6 @@ public class Application
/** Replaces the application directory and version in any argument. */
protected String processArg (String arg)
{
if (arg == null) {
return null;
}
arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath());
arg = arg.replace("%VERSION%", String.valueOf(_version));
@@ -1146,8 +1161,8 @@ public class Application
}
/**
* Loads the <code>digest.txt</code> file and verifies the contents of both that file and the
* <code>getdown.text</code> file. Then it loads the <code>version.txt</code> and decides
* Loads the {@code digest.txt} file and verifies the contents of both that file and the
* {@code getdown.text} file. Then it loads the {@code version.txt} and decides
* whether or not the application needs to be updated or whether we can proceed to verification
* and execution.
*
@@ -1234,11 +1249,11 @@ public class Application
}
if (_latest != null) {
try (InputStream in = ConnectionUtil.open(proxy, _latest, 0, 0).getInputStream();
InputStreamReader reader = new InputStreamReader(in, UTF_8);
BufferedReader bin = new BufferedReader(reader)) {
for (String[] pair : Config.parsePairs(bin, Config.createOpts(false))) {
if (pair[0].equals("version")) {
try {
List<String[]> vdata = Config.parsePairs(
new StringReader(conn.fetch(_latest)), Config.createOpts(false));
for (String[] pair : vdata) {
if ("version".equals(pair[0])) {
_targetVersion = Math.max(Long.parseLong(pair[1]), _targetVersion);
if (fileVersion != -1 && _targetVersion > fileVersion) {
// replace the file with the newest version
@@ -1444,7 +1459,7 @@ public class Application
protected URL createVAppBase (long version)
throws MalformedURLException
{
String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version);
String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", String.valueOf(version));
return HostWhitelist.verify(new URL(url));
}
@@ -1461,8 +1476,7 @@ public class Application
/**
* Downloads a new copy of CONFIG_FILE.
*/
protected void downloadConfigFile ()
throws IOException
protected void downloadConfigFile () throws IOException
{
downloadControlFile(CONFIG_FILE, 0);
}
@@ -1604,8 +1618,7 @@ public class Application
* Download a path to a temporary file, returning a {@link File} instance with the path
* contents.
*/
protected File downloadFile (String path)
throws IOException
protected File downloadFile (String path) throws IOException
{
File target = getLocalPath(path + "_new");
@@ -1615,30 +1628,11 @@ public class Application
} 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);
throw new IOException("Invalid path '" + path + "'.", 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);
}
}
}
conn.download(targetURL, target); // stream the URL into our temporary file
return target;
}
@@ -1652,9 +1646,7 @@ public class Application
/** Helper function to add all values in {@code values} (if non-null) to {@code target}. */
protected static void addAll (String[] values, List<String> target) {
if (values != null) {
for (String value : values) {
target.add(value);
}
Collections.addAll(target, values);
}
}
@@ -1737,7 +1729,6 @@ public class Application
}
protected final EnvConfig _envc;
protected File _config;
protected Digest _digest;
protected long _version = -1;
@@ -21,7 +21,7 @@ import static com.threerings.getdown.Log.log;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Manages the <code>digest.txt</code> file and the computing and processing of digests for an
* Manages the {@code digest.txt} file and the computing and processing of digests for an
* application.
*/
public class Digest
@@ -72,8 +72,7 @@ public class Digest
digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
completed.add(rsrc);
} catch (Throwable t) {
completed.add(new IOException("Error computing digest for: " + rsrc).
initCause(t));
completed.add(new IOException("Error computing digest for: " + rsrc, t));
}
}
});
@@ -88,7 +87,7 @@ public class Digest
if (done instanceof IOException) {
throw (IOException)done;
} else if (done instanceof Resource) {
pending.remove((Resource)done);
pending.remove(done);
} else {
throw new AssertionError("What is this? " + done);
}
@@ -19,7 +19,7 @@ public final class EnvConfig {
/** Used to report problems or feedback by {@link #create}. */
public static final class Note {
public static enum Level { INFO, WARN, ERROR };
public enum Level { INFO, WARN, ERROR }
public static Note info (String msg) { return new Note(Level.INFO, msg); }
public static Note warn (String msg) { return new Note(Level.WARN, msg); }
public static Note error (String msg) { return new Note(Level.ERROR, msg); }
@@ -23,7 +23,7 @@ import static com.threerings.getdown.Log.log;
public class Resource implements Comparable<Resource>
{
/** Defines special attributes for resources. */
public static enum Attr {
public enum Attr {
/** Indicates that the resource should be unpacked. */
UNPACK,
/** If present, when unpacking a resource, any directories created by the newly
@@ -35,7 +35,7 @@ public class Resource implements Comparable<Resource>
PRELOAD,
/** Indicates that the resource is a jar containing native libs. */
NATIVE
};
}
public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class);
public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK);
@@ -16,7 +16,7 @@ import com.threerings.getdown.util.VersionUtil;
* accessor so that it's easy to see all of the secret system property arguments that Getdown makes
* use of.
*/
public class SysProps
public final class SysProps
{
/** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */
public static String appDir () {
@@ -101,6 +101,16 @@ public class SysProps
return Boolean.getBoolean("direct");
}
/** If true, Getdown will always try to connect without proxy settings even it a proxy is set
* in {@code proxy.txt}. If direct access is possible it will not clear {@code proxy.txt}, it
* will preserve the settings. This is to support cases where a user uses a workstation in two
* different networks, one with proxy the other one without. They should not be asked for
* proxy settings again each time they switch back to the proxy network.
* Usage: {@code -Dtry_no_proxy}. */
public static boolean tryNoProxyFirst () {
return Boolean.getBoolean("try_no_proxy");
}
/** Specifies the connection timeout (in seconds) to use when downloading control files from
* the server. This is chiefly useful when you are running in versionless mode and want Getdown
* to more quickly timeout its startup update check if the server with which it is
@@ -0,0 +1,182 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.net;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.List;
import java.util.zip.GZIPInputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.threerings.getdown.data.SysProps;
import com.threerings.getdown.util.Base64;
import com.threerings.getdown.util.StreamUtil;
/**
* Manages the process of making HTTP connections, using a proxy if necessary. Tracks and reports
* when proxy credentials are rejected.
*/
public class Connector {
/** The default connector uses no proxy. */
public static final Connector DEFAULT = new Connector(Proxy.NO_PROXY);
/** Tracks the state of a connector. If it fails for proxy-related reasons, it may transition
* to a need_proxy or need_proxy_auth state. */
public enum State { ACTIVE, NEED_PROXY, NEED_PROXY_AUTH }
/** The proxy used by this connector. */
public final Proxy proxy;
/** The current state of this connector. */
public State state = State.ACTIVE;
public Connector (Proxy proxy) {
this.proxy = proxy;
}
/**
* Opens a connection to a URL, setting the authentication header if user info is present.
* @param proxy the proxy via which to perform HTTP connections.
* @param url the URL to which to open a connection.
* @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the
* connection. If {@code 0} is supplied, the connection timeout specified via system properties
* will be used instead.
* @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from
* the connection. If {@code 0} is supplied, the read timeout specified via system properties
* will be used instead.
*/
public URLConnection open (URL url, int connectTimeout, int readTimeout)
throws IOException
{
URLConnection conn = url.openConnection(proxy);
// configure a connect timeout, if requested
int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout();
if (ctimeout > 0) {
conn.setConnectTimeout(ctimeout * 1000);
}
// configure a read timeout, if requested
int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout();
if (rtimeout > 0) {
conn.setReadTimeout(rtimeout * 1000);
}
// If URL has a username:password@ before hostname, use HTTP basic auth
String userInfo = url.getUserInfo();
if (userInfo != null) {
// Remove any percent-encoding in the username/password
userInfo = URLDecoder.decode(userInfo, "UTF-8");
// Now base64 encode the auth info and make it a single line
String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT).
replaceAll("\\n","").replaceAll("\\r", "");
conn.setRequestProperty("Authorization", "Basic " + encoded);
}
return conn;
}
/**
* Opens a connection to a http or https URL, setting the authentication header if user info is
* present. Throws a class cast exception if the connection returned is not the right type. See
* {@link #open} for parameter documentation.
*/
public HttpURLConnection openHttp (URL url, int connectTimeout, int readTimeout)
throws IOException
{
return (HttpURLConnection)open(url, connectTimeout, readTimeout);
}
/**
* Downloads {@code url} into {@code target}.
*/
public void download (URL url, File target) throws IOException {
URLConnection conn = open(url, 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
conn.setUseCaches(false);
conn.setRequestProperty("Accept-Encoding", "gzip");
checkConnectOK(conn, "Unable to download " + url);
try (InputStream fin = conn.getInputStream()) {
String encoding = conn.getContentEncoding();
boolean gzip = "gzip".equalsIgnoreCase(encoding);
try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) {
try (FileOutputStream fout = new FileOutputStream(target)) {
StreamUtil.copy(fin2, fout);
}
}
}
}
/**
* Fetches the data at {@code url} into a string.
*/
public String fetch (URL url) throws IOException {
URLConnection conn = open(url, 0, 0);
checkConnectOK(conn, "Unable to fetch " + url);
int size = conn.getContentLength();
ByteArrayOutputStream out = new ByteArrayOutputStream(size > 0 ? size : 1024);
try (InputStream in = conn.getInputStream()) {
StreamUtil.copy(in, out);
}
return out.toString(UTF_8.toString());
}
/**
* Checks that {@code conn} returned an {@code OK} response code iff it is an HTTP connection.
* If the connection failed for proxy related reasons, this changes the state of this connector
* to reflect the needed proxy information.
*/
public void checkConnectOK (URLConnection conn, String errpre) throws IOException
{
// if it's not an HTTP connection, there's nothing to check
if (!(conn instanceof HttpURLConnection)) return;
int code = ((HttpURLConnection)conn).getResponseCode();
switch (code) {
case HttpURLConnection.HTTP_OK:
return;
case HttpURLConnection.HTTP_FORBIDDEN:
case HttpURLConnection.HTTP_USE_PROXY:
state = State.NEED_PROXY;
break;
case HttpURLConnection.HTTP_PROXY_AUTH:
state = State.NEED_PROXY_AUTH;
break;
}
throw new IOException(errpre + " [code=" + code + "]");
}
/**
* Adds appropriate proxy args from this connector's configuration to the supplied list of
* command line args that will be used to launch the app.
*/
public void addProxyArgs (List<String> args) {
if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) {
InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address();
String proxyHost = proxyAddr.getHostString();
int proxyPort = proxyAddr.getPort();
args.add("-Dhttp.proxyHost=" + proxyHost);
args.add("-Dhttp.proxyPort=" + proxyPort);
args.add("-Dhttps.proxyHost=" + proxyHost);
args.add("-Dhttps.proxyPort=" + proxyPort);
}
}
}
@@ -6,7 +6,13 @@
package com.threerings.getdown.net;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.Collection;
import java.util.HashMap;
@@ -26,8 +32,13 @@ import static com.threerings.getdown.Log.log;
* implementors must take care to only execute thread-safe code or simply pass a message to the AWT
* thread, for example.
*/
public abstract class Downloader
public class Downloader
{
public Downloader (Connector conn)
{
_conn = conn;
}
/**
* Start the downloading process.
* @param resources the resources to download.
@@ -132,7 +143,22 @@ public abstract class Downloader
/**
* Performs the protocol-specific portion of checking download size.
*/
protected abstract long checkSize (Resource rsrc) throws IOException;
protected long checkSize (Resource rsrc) throws IOException {
URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0);
try {
// if we're accessing our data via HTTP, we only need a HEAD request
if (conn instanceof HttpURLConnection) {
((HttpURLConnection)conn).setRequestMethod("HEAD");
}
// make sure we got a satisfactory response code
_conn.checkConnectOK(conn, "Unable to check up-to-date for " + rsrc.getRemote());
return conn.getContentLength();
} finally {
// let it be known that we're done with this connection
conn.getInputStream().close();
}
}
/**
* Periodically called by the protocol-specific downloaders to update their progress. This
@@ -203,13 +229,62 @@ public abstract class Downloader
* protocol-specific code. This method should periodically check whether {@code _state} is set
* to aborted and abort any in-progress download if so.
*/
protected abstract void download (Resource rsrc) throws IOException;
protected void download (Resource rsrc) throws IOException {
URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0);
// make sure we got a satisfactory response code
_conn.checkConnectOK(conn, "Unable to download resource " + rsrc.getRemote());
// TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
// system property
if (true) {
// download the resource from the specified URL
long actualSize = conn.getContentLength();
log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
long currentSize = 0L;
byte[] buffer = new byte[4*4096];
try (InputStream in = conn.getInputStream();
FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
// TODO: look to see if we have a download info file
// containing info on potentially partially downloaded data;
// if so, use a "Range: bytes=HAVE-" header.
// read in the file data
int read;
while ((read = in.read(buffer)) != -1) {
// abort the download if the downloader is aborted
if (_state == State.ABORTED) {
break;
}
// write it out to our local copy
out.write(buffer, 0, read);
// note that we've downloaded some data
currentSize += read;
reportProgress(rsrc, currentSize, actualSize);
}
}
} else {
log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
File localNew = rsrc.getLocalNew();
try (ReadableByteChannel rbc = Channels.newChannel(conn.getInputStream());
FileOutputStream fos = new FileOutputStream(localNew)) {
// TODO: more work is needed here, transferFrom can fail to transfer the entire
// file, in which case it's not clear what we're supposed to do.. call it again?
// will it repeatedly fail?
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
reportProgress(rsrc, localNew.length(), localNew.length());
}
}
}
protected final Connector _conn;
/** The reported sizes of our resources. */
protected Map<Resource, Long> _sizes = new HashMap<>();
protected final Map<Resource, Long> _sizes = new HashMap<>();
/** The bytes downloaded for each resource. */
protected Map<Resource, Long> _downloaded = new HashMap<>();
protected final Map<Resource, Long> _downloaded = new HashMap<>();
/** The time at which the file transfer began. */
protected long _start;
@@ -1,115 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.net;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.ConnectionUtil;
import static com.threerings.getdown.Log.log;
/**
* Implements downloading files over HTTP
*/
public class HTTPDownloader extends Downloader
{
public HTTPDownloader (Proxy proxy)
{
_proxy = proxy;
}
@Override protected long checkSize (Resource rsrc) throws IOException
{
URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0);
try {
// if we're accessing our data via HTTP, we only need a HEAD request
if (conn instanceof HttpURLConnection) {
HttpURLConnection hcon = (HttpURLConnection)conn;
hcon.setRequestMethod("HEAD");
hcon.connect();
// make sure we got a satisfactory response code
if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("Unable to check up-to-date for " +
rsrc.getRemote() + ": " + hcon.getResponseCode());
}
}
return conn.getContentLength();
} finally {
// let it be known that we're done with this connection
conn.getInputStream().close();
}
}
@Override protected void download (Resource rsrc) throws IOException
{
// TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
// system property
if (true) {
// download the resource from the specified URL
URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0);
conn.connect();
// make sure we got a satisfactory response code
if (conn instanceof HttpURLConnection) {
HttpURLConnection hcon = (HttpURLConnection)conn;
if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " +
hcon.getResponseCode());
}
}
long actualSize = conn.getContentLength();
log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
long currentSize = 0L;
byte[] buffer = new byte[4*4096];
try (InputStream in = conn.getInputStream();
FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
// TODO: look to see if we have a download info file
// containing info on potentially partially downloaded data;
// if so, use a "Range: bytes=HAVE-" header.
// read in the file data
int read;
while ((read = in.read(buffer)) != -1) {
// abort the download if the downloader is aborted
if (_state == State.ABORTED) {
break;
}
// write it out to our local copy
out.write(buffer, 0, read);
// note that we've downloaded some data
currentSize += read;
reportProgress(rsrc, currentSize, actualSize);
}
}
} else {
log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
File localNew = rsrc.getLocalNew();
try (ReadableByteChannel rbc = Channels.newChannel(rsrc.getRemote().openStream());
FileOutputStream fos = new FileOutputStream(localNew)) {
// TODO: more work is needed here, transferFrom can fail to transfer the entire
// file, in which case it's not clear what we're supposed to do.. call it again?
// will it repeatedly fail?
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
reportProgress(rsrc, localNew.length(), localNew.length());
}
}
}
protected final Proxy _proxy;
}
@@ -11,7 +11,7 @@ package com.threerings.getdown.spi;
public interface ProxyAuth
{
/** Credentials for a proxy server. */
public static class Credentials {
class Credentials {
public final String username;
public final String password;
public Credentials (String username, String password) {
@@ -23,10 +23,10 @@ public interface ProxyAuth
/**
* Loads the credentials for the app installed in {@code appDir}.
*/
public Credentials loadCredentials (String appDir);
Credentials loadCredentials (String appDir);
/**
* Encrypts and saves the credentials for the app installed in {@code appDir}.
*/
public void saveCredentials (String appDir, String username, String password);
void saveCredentials (String appDir, String username, String password);
}
@@ -5,10 +5,17 @@
package com.threerings.getdown.tools;
import java.io.*;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
@@ -31,8 +38,8 @@ public class Differ
/**
* Creates a single patch file that contains the differences between
* the two specified application directories. The patch file will be
* created in the <code>nvdir</code> directory with name
* <code>patchV.dat</code> where V is the old application version.
* created in the {@code nvdir} directory with name
* {@code patchV.dat} where V is the old application version.
*/
public void createDiff (File nvdir, File ovdir, boolean verbose)
throws IOException
@@ -53,13 +60,13 @@ public class Differ
Application oapp = new Application(new EnvConfig(ovdir));
oapp.init(false);
ArrayList<Resource> orsrcs = new ArrayList<>();
List<Resource> orsrcs = new ArrayList<>();
orsrcs.addAll(oapp.getCodeResources());
orsrcs.addAll(oapp.getResources());
Application napp = new Application(new EnvConfig(nvdir));
napp.init(false);
ArrayList<Resource> nrsrcs = new ArrayList<>();
List<Resource> nrsrcs = new ArrayList<>();
nrsrcs.addAll(napp.getCodeResources());
nrsrcs.addAll(napp.getResources());
@@ -83,8 +90,8 @@ public class Differ
}
}
protected void createPatch (File patch, ArrayList<Resource> orsrcs,
ArrayList<Resource> nrsrcs, boolean verbose)
protected void createPatch (File patch, List<Resource> orsrcs,
List<Resource> nrsrcs, boolean verbose)
throws IOException
{
int version = Digest.VERSION;
@@ -200,7 +207,7 @@ public class Differ
Differ differ = new Differ();
boolean verbose = false;
int aidx = 0;
if (args[0].equals("-verbose")) {
if ("-verbose".equals(args[0])) {
verbose = true;
aidx++;
}
@@ -23,6 +23,7 @@ import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.EnvConfig;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.Base64;
import com.threerings.getdown.util.Config;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -74,8 +75,11 @@ public class Digester
System.out.println("Generating digest file '" + target + "'...");
// create our application and instruct it to parse its business
Application app = new Application(new EnvConfig(appdir));
app.init(false);
EnvConfig envc = new EnvConfig(appdir);
Application app = new Application(envc);
Config config = Application.readConfig(envc, false);
app.initBase(config);
app.initResources(config);
List<Resource> rsrcs = new ArrayList<>();
rsrcs.add(app.getConfigResource());
@@ -86,6 +90,9 @@ public class Digester
rsrcs.addAll(ag.rsrcs);
}
// reinit app just to verify that getdown.txt has valid format
app.init(true);
// now generate the digest file
Digest.createDigest(version, rsrcs, target);
}
@@ -67,15 +67,15 @@ import static java.nio.charset.StandardCharsets.UTF_8;
public class JarDiff implements JarDiffCodes
{
private static final int DEFAULT_READ_SIZE = 2048;
private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
private static final byte[] newBytes = new byte[DEFAULT_READ_SIZE];
private static final byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
// The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java
// and other stuff here. Instead, we use an explicit _debug flag.
private static boolean _debug;
/**
* Creates a patch from the two passed in files, writing the result to <code>os</code>.
* Creates a patch from the two passed in files, writing the result to {@code os}.
*/
public static void createPatch (String oldPath, String newPath,
OutputStream os, boolean minimal) throws IOException
@@ -83,10 +83,10 @@ public class JarDiff implements JarDiffCodes
try (ZipFile2 oldArchive = new ZipFile2(oldPath);
ZipFile2 newArchive = new ZipFile2(newPath)) {
HashMap<String,String> moved = new HashMap<>();
HashSet<String> implicit = new HashSet<>();
HashSet<String> moveSrc = new HashSet<>();
HashSet<String> newEntries = new HashSet<>();
Map<String,String> moved = new HashMap<>();
Set<String> implicit = new HashSet<>();
Set<String> moveSrc = new HashSet<>();
Set<String> newEntries = new HashSet<>();
// FIRST PASS
// Go through the entries in new archive and determine which files are candidates for
@@ -157,7 +157,7 @@ public class JarDiff implements JarDiffCodes
// SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> -
// <source of move commands> - <new or modified entries>
ArrayList<String> deleted = new ArrayList<>();
List<String> deleted = new ArrayList<>();
for (ZipEntry oldEntry : oldArchive) {
String oldName = oldEntry.getName();
if (!implicit.contains(oldName) && !moveSrc.contains(oldName)
@@ -203,9 +203,9 @@ public class JarDiff implements JarDiffCodes
}
/**
* Writes the index file out to <code>jos</code>.
* <code>oldEntries</code> gives the names of the files that were removed,
* <code>movedMap</code> maps from the new name to the old name.
* Writes the index file out to {@code jos}.
* {@code oldEntries} gives the names of the files that were removed,
* {@code movedMap} maps from the new name to the old name.
*/
private static void createIndex (ZipOutputStream jos, List<String> oldEntries,
Map<String,String> movedMap)
@@ -286,7 +286,7 @@ public class JarDiff implements JarDiffCodes
*/
private static class ZipFile2 implements Iterable<ZipEntry>, Closeable
{
private ZipFile _archive;
private final ZipFile _archive;
private List<ZipEntry> _entries;
private HashMap<String,ZipEntry> _nameToEntryMap;
private HashMap<Long,LinkedList<ZipEntry>> _crcToEntryMap;
@@ -384,7 +384,7 @@ public class JarDiff implements JarDiffCodes
public String hasSameContent (ZipFile2 file, ZipEntry entry) throws IOException {
String thisName = null;
Long crcL = Long.valueOf(entry.getCrc());
Long crcL = entry.getCrc();
// check if this archive contains files with the passed in entry's crc
if (_crcToEntryMap.containsKey(crcL)) {
// get the Linked List with files with the crc
@@ -419,7 +419,7 @@ public class JarDiff implements JarDiffCodes
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
long crc = entry.getCrc();
Long crcL = Long.valueOf(crc);
Long crcL = crc;
if (_debug) {
System.out.println("\t" + entry.getName() + " CRC " + crc);
}
@@ -215,7 +215,7 @@ public class JarDiffPatcher implements JarDiffCodes
{
int index = 0;
int length = path.length();
ArrayList<String> sub = new ArrayList<>();
List<String> sub = new ArrayList<>();
while (index < length) {
while (index < length && Character.isWhitespace
@@ -223,9 +223,8 @@ public class JarDiffPatcher implements JarDiffCodes
index++;
}
if (index < length) {
int start = index;
int last = start;
String subString = null;
int last = index;
StringBuilder subString = null;
while (index < length) {
char aChar = path.charAt(index);
@@ -233,9 +232,9 @@ public class JarDiffPatcher implements JarDiffCodes
path.charAt(index + 1) == ' ') {
if (subString == null) {
subString = path.substring(last, index);
subString = new StringBuilder(path.substring(last, index));
} else {
subString += path.substring(last, index);
subString.append(path, last, index);
}
last = ++index;
} else if (Character.isWhitespace(aChar)) {
@@ -245,12 +244,14 @@ public class JarDiffPatcher implements JarDiffCodes
}
if (last != index) {
if (subString == null) {
subString = path.substring(last, index);
subString = new StringBuilder(path.substring(last, index));
} else {
subString += path.substring(last, index);
subString.append(path, last, index);
}
}
sub.add(subString);
if (subString != null) {
sub.add(subString.toString());
}
}
}
return sub;
@@ -24,7 +24,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
*/
public class Base64 {
public final class Base64 {
/**
* Default values for encoder/decoder flags.
*/
@@ -68,7 +68,7 @@ public class Base64 {
// shared code
// --------------------------------------------------------
/* package */ static abstract class Coder {
/* package */ abstract static class Coder {
public byte[] output;
public int op;
@@ -178,7 +178,7 @@ public class Base64 {
* Lookup table for turning bytes into their position in the
* Base64 alphabet.
*/
private static final int DECODE[] = {
private static final int[] DECODE = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
@@ -201,7 +201,7 @@ public class Base64 {
* Decode lookup table for the "web safe" variant (RFC 3548
* sec. 4) where - and _ replace + and /.
*/
private static final int DECODE_WEBSAFE[] = {
private static final int[] DECODE_WEBSAFE = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
@@ -236,7 +236,7 @@ public class Base64 {
private int state; // state number (0 to 6)
private int value;
final private int[] alphabet;
private final int[] alphabet;
public Decoder(int flags, byte[] output) {
this.output = output;
@@ -541,7 +541,7 @@ public class Base64 {
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private static final byte ENCODE[] = {
private static final byte[] ENCODE = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
@@ -552,21 +552,21 @@ public class Base64 {
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private static final byte ENCODE_WEBSAFE[] = {
private static final byte[] ENCODE_WEBSAFE = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
};
final private byte[] tail;
private final byte[] tail;
/* package */ int tailLen;
private int count;
final public boolean do_padding;
final public boolean do_newline;
final public boolean do_cr;
final private byte[] alphabet;
public final boolean do_padding;
public final boolean do_newline;
public final boolean do_cr;
private final byte[] alphabet;
public Encoder(int flags, byte[] output) {
this.output = output;
@@ -618,7 +618,7 @@ public class Base64 {
((input[p++] & 0xff) << 8) |
(input[p++] & 0xff);
tailLen = 0;
};
}
break;
case 2:
@@ -8,11 +8,11 @@ package com.threerings.getdown.util;
/**
* Utilities for handling ARGB colors.
*/
public class Color
public final class Color
{
public final static int CLEAR = 0x00000000;
public final static int WHITE = 0xFFFFFFFF;
public final static int BLACK = 0xFF000000;
public static final int CLEAR = 0x00000000;
public static final int WHITE = 0xFFFFFFFF;
public static final int BLACK = 0xFF000000;
public static float brightness (int argb) {
// TODO: we're ignoring alpha here...
@@ -62,7 +62,7 @@ public class Config
*
* @param opts options that influence the parsing. See {@link #createOpts}.
*
* @return a list of <code>String[]</code> instances containing the key/value pairs in the
* @return a list of {@code String[]} instances containing the key/value pairs in the
* order they were parsed from the file.
*/
public static List<String[]> parsePairs (File source, ParseOpts opts)
@@ -83,7 +83,7 @@ public class Config
List<String[]> pairs = new ArrayList<>();
for (String line : FileUtil.readLines(source)) {
// nix comments
int cidx = line.indexOf("#");
int cidx = line.indexOf('#');
if (opts.strictComments ? cidx == 0 : cidx != -1) {
line = line.substring(0, cidx);
}
@@ -97,7 +97,7 @@ public class Config
// parse our key/value pair
String[] pair = new String[2];
// if we're biasing toward key, put all the extra = in the key rather than the value
int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
int eidx = opts.biasToKey ? line.lastIndexOf('=') : line.indexOf('=');
if (eidx != -1) {
pair[0] = line.substring(0, eidx).trim();
pair[1] = line.substring(eidx+1).trim();
@@ -108,7 +108,7 @@ public class Config
// if the pair has an os qualifier, we need to process it
if (pair[1].startsWith("[")) {
int qidx = pair[1].indexOf("]");
int qidx = pair[1].indexOf(']');
if (qidx == -1) {
log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
continue; // omit the pair entirely
@@ -351,7 +351,7 @@ public class Config
protected static boolean checkQualifiers (String quals, String osname, String osarch)
{
if (quals.startsWith("!")) {
if (quals.indexOf(",") != -1) { // sanity check
if (quals.contains(",")) { // sanity check
log.warning("Multiple qualifiers cannot be used when one of the qualifiers " +
"is negative", "quals", quals);
return false;
@@ -371,7 +371,7 @@ public class Config
{
String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-");
String os = bits[0], arch = (bits.length > 1) ? bits[1] : "";
return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1);
return (osname.contains(os)) && (osarch.contains(arch));
}
private final Map<String, Object> _data;
@@ -1,73 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import com.threerings.getdown.data.SysProps;
import static java.nio.charset.StandardCharsets.UTF_8;
public class ConnectionUtil
{
/**
* Opens a connection to a URL, setting the authentication header if user info is present.
* @param proxy the proxy via which to perform HTTP connections.
* @param url the URL to which to open a connection.
* @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the
* connection. If {@code 0} is supplied, the connection timeout specified via system properties
* will be used instead.
* @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from
* the connection. If {@code 0} is supplied, the read timeout specified via system properties
* will be used instead.
*/
public static URLConnection open (Proxy proxy, URL url, int connectTimeout, int readTimeout)
throws IOException
{
URLConnection conn = url.openConnection(proxy);
// configure a connect timeout, if requested
int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout();
if (ctimeout > 0) {
conn.setConnectTimeout(ctimeout * 1000);
}
// configure a read timeout, if requested
int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout();
if (rtimeout > 0) {
conn.setReadTimeout(rtimeout * 1000);
}
// If URL has a username:password@ before hostname, use HTTP basic auth
String userInfo = url.getUserInfo();
if (userInfo != null) {
// Remove any percent-encoding in the username/password
userInfo = URLDecoder.decode(userInfo, "UTF-8");
// Now base64 encode the auth info and make it a single line
String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT).
replaceAll("\\n","").replaceAll("\\r", "");
conn.setRequestProperty("Authorization", "Basic " + encoded);
}
return conn;
}
/**
* Opens a connection to a http or https URL, setting the authentication header if user info is
* present. Throws a class cast exception if the connection returned is not the right type. See
* {@link #open} for parameter documentation.
*/
public static HttpURLConnection openHttp (
Proxy proxy, URL url, int connectTimeout, int readTimeout) throws IOException
{
return (HttpURLConnection)open(proxy, url, connectTimeout, readTimeout);
}
}
@@ -17,19 +17,19 @@ import static com.threerings.getdown.Log.log;
* Useful routines for launching Java applications from within other Java
* applications.
*/
public class LaunchUtil
public final class LaunchUtil
{
/** The default directory into which a local VM installation should be unpacked. */
public static final String LOCAL_JAVA_DIR = "java_vm";
/**
* Writes a <code>version.txt</code> file into the specified application directory and
* Writes a {@code version.txt} file into the specified application directory and
* attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly
* specified version and relaunch the application.
*
* @param appdir the directory in which the application is installed.
* @param getdownJarName the name of the getdown jar file in the application directory. This is
* probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using
* probably {@code getdown-pro.jar} or {@code getdown-retro-pro.jar} if you are using
* the results of the standard build.
* @param newVersion the new version to which Getdown will update when it is executed.
* @param javaLocalDir the name of the directory (inside {@code appdir}) that contains a
@@ -41,7 +41,7 @@ public class LaunchUtil
* after making this call as it will be upgraded and restarted. If false is returned, the
* application should tell the user that they must restart the application manually.
*
* @exception IOException thrown if we were unable to create the <code>version.txt</code> file
* @exception IOException thrown if we were unable to create the {@code version.txt} file
* in the supplied application directory. If the version.txt file cannot be created, restarting
* Getdown will not cause the application to be upgraded, so the application will have to
* resort to telling the user that it is in a bad way.
@@ -177,23 +177,23 @@ public class LaunchUtil
public static boolean mustMonitorChildren ()
{
String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
return (osname.contains("windows 98") || osname.contains("windows me"));
}
/**
* Returns true if we're running in a JVM that identifies its operating system as Windows.
*/
public static final boolean isWindows () { return _isWindows; }
public static boolean isWindows () { return _isWindows; }
/**
* Returns true if we're running in a JVM that identifies its operating system as MacOS.
*/
public static final boolean isMacOS () { return _isMacOS; }
public static boolean isMacOS () { return _isMacOS; }
/**
* Returns true if we're running in a JVM that identifies its operating system as Linux.
*/
public static final boolean isLinux () { return _isLinux; }
public static boolean isLinux () { return _isLinux; }
/**
* Checks whether a Java Virtual Machine can be located in the supplied path.
@@ -232,10 +232,9 @@ public class LaunchUtil
try {
String osname = System.getProperty("os.name");
osname = (osname == null) ? "" : osname;
_isWindows = (osname.indexOf("Windows") != -1);
_isMacOS = (osname.indexOf("Mac OS") != -1 ||
osname.indexOf("MacOS") != -1);
_isLinux = (osname.indexOf("Linux") != -1);
_isWindows = (osname.contains("Windows"));
_isMacOS = (osname.contains("Mac OS") || osname.contains("MacOS"));
_isLinux = (osname.contains("Linux"));
} catch (Exception e) {
// can't grab system properties; we'll just pretend we're not on any of these OSes
}
@@ -5,7 +5,7 @@
package com.threerings.getdown.util;
public class MessageUtil {
public final class MessageUtil {
/**
* Returns whether or not the provided string is tainted. See {@link #taint}. Null strings
@@ -101,11 +101,11 @@ public class MessageUtil {
}
/**
* Used to escape single quotes so that they are not interpreted by <code>MessageFormat</code>.
* Used to escape single quotes so that they are not interpreted by {@code MessageFormat}.
* As we assume all single quotes are to be escaped, we cannot use the characters
* <code>{</code> and <code>}</code> in our translation strings, but this is a small price to
* pay to have to differentiate between messages that will and won't eventually be parsed by a
* <code>MessageFormat</code> instance.
* {@code MessageFormat} instance.
*/
public static String escape (String message)
{
@@ -14,5 +14,5 @@ public interface ProgressObserver
* Informs the observer that we have completed the specified
* percentage of the process.
*/
public void progress (int percent);
void progress (int percent);
}
@@ -11,11 +11,10 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import static com.threerings.getdown.Log.log;
public class StreamUtil {
public final class StreamUtil {
/**
* Convenient close for a stream. Use in a finally clause and love life.
*/
@@ -7,14 +7,14 @@ package com.threerings.getdown.util;
import java.util.StringTokenizer;
public class StringUtil {
public final class StringUtil {
/**
* @return true if the specified string could be a valid URL (contains no illegal characters)
*/
public static boolean couldBeValidUrl (String url)
{
return url.matches("[A-Za-z0-9\\-\\._~:/\\?#\\[\\]@!$&'\\(\\)\\*\\+,;=%]+");
return url.matches("[A-Za-z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+");
}
/**
@@ -84,7 +84,7 @@ public class StringUtil {
source = source.replace(",,", "%COMMA%");
// count up the number of tokens
while ((tpos = source.indexOf(",", tpos+1)) != -1) {
while ((tpos = source.indexOf(',', tpos+1)) != -1) {
tcount++;
}
@@ -92,7 +92,7 @@ public class StringUtil {
tpos = -1; tcount = 0;
// do the split
while ((tpos = source.indexOf(",", tpos+1)) != -1) {
while ((tpos = source.indexOf(',', tpos+1)) != -1) {
tokens[tcount] = source.substring(tstart, tpos);
tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ",");
if (intern) {
@@ -119,7 +119,7 @@ public class StringUtil {
/**
* Generates a string from the supplied bytes that is the HEX encoded representation of those
* bytes. Returns the empty string for a <code>null</code> or empty byte array.
* bytes. Returns the empty string for a {@code null} or empty byte array.
*
* @param bytes the bytes for which we want a string representation.
* @param count the number of bytes to stop at (which will be coerced into being {@code <=} the
@@ -185,7 +185,7 @@ public class StringUtil {
}
/**
* Helper function for the various <code>join</code> methods.
* Helper function for the various {@code join} methods.
*/
protected static String join (Object[] values, String separator, boolean escape)
{
@@ -201,6 +201,6 @@ public class StringUtil {
return buf.toString();
}
/** Used by {@link #hexlate} and {@link #unhexlate}. */
/** Used by {@link #hexlate}. */
protected static final String XLATE = "0123456789abcdef";
}
@@ -22,7 +22,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Version related utilities.
*/
public class VersionUtil
public final class VersionUtil
{
/**
* Reads a version number from a file.
+7
View File
@@ -38,6 +38,12 @@
<version>1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -189,6 +195,7 @@
<lib>${java.home}/jmods/java.base.jmod</lib>
<lib>${java.home}/jmods/java.desktop.jmod</lib>
<lib>${java.home}/jmods/java.logging.jmod</lib>
<lib>${java.home}/jmods/java.scripting.jmod</lib>
<lib>${java.home}/jmods/jdk.jsobject.jmod</lib>
</libs>
</configuration>
@@ -72,7 +72,7 @@ public final class AbortPanel extends JFrame
public void actionPerformed (ActionEvent e)
{
String cmd = e.getActionCommand();
if (cmd.equals("ok")) {
if ("ok".equals(cmd)) {
System.exit(0);
} else {
setVisible(false);
@@ -22,7 +22,15 @@ import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
@@ -31,24 +39,44 @@ import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.getdown.data.*;
import com.threerings.getdown.data.Application.UpdateInterface.Step;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Build;
import com.threerings.getdown.data.EnvConfig;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.data.SysProps;
import com.threerings.getdown.net.Connector;
import com.threerings.getdown.net.Downloader;
import com.threerings.getdown.net.HTTPDownloader;
import com.threerings.getdown.tools.Patcher;
import com.threerings.getdown.util.*;
import com.threerings.getdown.util.Config;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.LaunchUtil;
import com.threerings.getdown.util.MessageUtil;
import com.threerings.getdown.util.ProgressAggregator;
import com.threerings.getdown.util.ProgressObserver;
import com.threerings.getdown.util.StringUtil;
import com.threerings.getdown.util.VersionUtil;
import static com.threerings.getdown.Log.log;
/**
* Manages the main control for the Getdown application updater and deployment system.
*/
public abstract class Getdown extends Thread
public abstract class Getdown
implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
{
/**
* Starts a thread to run Getdown and ultimately (hopefully) launch the target app.
*/
public static void run (final Getdown getdown) {
new Thread("Getdown") {
@Override public void run () {
getdown.run();
}
}.start();
}
public Getdown (EnvConfig envc)
{
super("Getdown");
try {
// If the silent property exists, install without bringing up any gui. If it equals
// launch, start the application after installing. Otherwise, just install and exit.
@@ -75,7 +103,7 @@ public abstract class Getdown extends Thread
// welcome to hell, where java can't cope with a classpath that contains jars that live
// in a directory that contains a !, at least the same bug happens on all platforms
String dir = envc.appDir.toString();
if (dir.equals(".")) {
if (".".equals(dir)) {
dir = System.getProperty("user.dir");
}
String errmsg = "The directory in which this application is installed:\n" + dir +
@@ -115,8 +143,28 @@ public abstract class Getdown extends Thread
}
}
@Override
public void run ()
/**
* Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
*/
public void configProxy (String host, String port, String username, String password)
{
log.info("User configured proxy", "host", host, "port", port);
ProxyUtil.configProxy(_app, host, port, username, password);
// clear out our UI
disposeContainer();
_container = null;
// fire up a new thread
run(this);
}
/**
* The main entry point of Getdown: does some sanity checks and preparation, then delegates the
* actual getting down to {@link #getdown}. This is not called directly, but rather via the
* static {@code run} method as Getdown does its main work on a separate thread.
*/
protected void run ()
{
// if we have no messages, just bail because we're hosed; the error message will be
// displayed to the user already
@@ -130,79 +178,27 @@ public abstract class Getdown extends Thread
File instdir = _app.getLocalPath("");
if (!instdir.canWrite()) {
String path = instdir.getPath();
if (path.equals(".")) {
if (".".equals(path)) {
path = System.getProperty("user.dir");
}
fail(MessageUtil.tcompose("m.readonly_error", path));
return;
}
try {
_dead = false;
// if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
// run the app anyway because we're prepared to cope with not being able to update
if (detectProxy() || _app.allowOffline()) {
getdown();
} else if (_silent) {
log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
} else {
// create a panel they can use to configure the proxy settings
_container = createContainer();
// allow them to close the window to abort the proxy configuration
_dead = true;
configureContainer();
ProxyPanel panel = new ProxyPanel(this, _msgs);
// set up any existing configured proxy
String[] hostPort = ProxyUtil.loadProxy(_app);
panel.setProxy(hostPort[0], hostPort[1]);
_container.add(panel, BorderLayout.CENTER);
showContainer();
}
} catch (Exception e) {
log.warning("run() failed.", e);
String msg = e.getMessage();
if (msg == null) {
msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
} else if (!msg.startsWith("m.")) {
// try to do something sensible based on the type of error
if (e instanceof FileNotFoundException) {
msg = MessageUtil.compose(
"m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
} else {
msg = MessageUtil.compose(
"m.init_error", MessageUtil.taint(msg), _ifc.installError);
}
}
fail(msg);
}
}
/**
* Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
*/
public void configProxy (String host, String port, String username, String password)
{
log.info("User configured proxy", "host", host, "port", port);
if (!StringUtil.isBlank(host)) {
ProxyUtil.configProxy(_app, host, port, username, password);
}
// clear out our UI
disposeContainer();
_container = null;
// fire up a new thread
new Thread(this).start();
_dead = false;
// if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
// run the app anyway because we're prepared to cope with not being able to update
if (detectProxy() || _app.allowOffline()) getdown();
else requestProxyInfo(false);
}
protected boolean detectProxy () {
if (ProxyUtil.autoDetectProxy(_app)) {
boolean tryNoProxy = SysProps.tryNoProxyFirst();
if (!tryNoProxy && ProxyUtil.autoDetectProxy(_app)) {
return true;
}
// otherwise see if we actually need a proxy; first we have to initialize our application
// see if we actually need a proxy; first we have to initialize our application
// to get some sort of interface configuration and the appbase URL
log.info("Checking whether we need to use a proxy...");
try {
@@ -211,14 +207,18 @@ public abstract class Getdown extends Thread
// no worries
}
updateStatus("m.detecting_proxy");
if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
return false;
URL configURL = _app.getConfigResource().getRemote();
if (!ProxyUtil.canLoadWithoutProxy(configURL, tryNoProxy ? 2 : 5)) {
// if we didn't auto-detect proxy first thing, do auto-detect now
return tryNoProxy ? ProxyUtil.autoDetectProxy(_app) : false;
}
// we got through, so we appear not to require a proxy; make a blank proxy config so that
// we don't go through this whole detection process again next time
log.info("No proxy appears to be needed.");
ProxyUtil.saveProxy(_app, null, null);
if (!tryNoProxy) {
// we got through, so we appear not to require a proxy; make a blank proxy config so
// that we don't go through this whole detection process again next time
ProxyUtil.saveProxy(_app, null, null);
}
return true;
}
@@ -228,6 +228,25 @@ public abstract class Getdown extends Thread
_ifc = new Application.UpdateInterface(config);
}
protected void requestProxyInfo (boolean reinitAuth) {
if (_silent) {
log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
return;
}
// create a panel they can use to configure the proxy settings
_container = createContainer();
// allow them to close the window to abort the proxy configuration
_dead = true;
configureContainer();
ProxyPanel panel = new ProxyPanel(this, _msgs, reinitAuth);
// set up any existing configured proxy
String[] hostPort = ProxyUtil.loadProxy(_app);
panel.setProxy(hostPort[0], hostPort[1]);
_container.add(panel, BorderLayout.CENTER);
showContainer();
}
/**
* Downloads and installs (without verifying) any resources that are marked with a
* {@code PRELOAD} attribute.
@@ -258,7 +277,7 @@ public abstract class Getdown extends Thread
protected void getdown ()
{
try {
// first parses our application deployment file
// first parse our application deployment file
try {
readConfig(true);
} catch (IOException ioe) {
@@ -273,7 +292,7 @@ public abstract class Getdown extends Thread
throw new MultipleGetdownRunning();
}
// Update the config modtime so a sleeping getdown will notice the change.
// update the config modtime so a sleeping getdown will notice the change
File config = _app.getLocalPath(Application.CONFIG_FILE);
if (!config.setLastModified(System.currentTimeMillis())) {
log.warning("Unable to set modtime on config file, will be unable to check for " +
@@ -285,7 +304,7 @@ public abstract class Getdown extends Thread
// Store the config modtime before waiting the delay amount of time
long lastConfigModtime = config.lastModified();
log.info("Waiting " + _delay + " minutes before beginning actual work.");
Thread.sleep(_delay * 60 * 1000);
TimeUnit.MINUTES.sleep(_delay);
if (lastConfigModtime < config.lastModified()) {
log.warning("getdown.txt was modified while getdown was waiting.");
throw new MultipleGetdownRunning();
@@ -334,11 +353,7 @@ public abstract class Getdown extends Thread
if (toDownload.size() > 0) {
// we have resources to download, also note them as to-be-installed
for (Resource r : toDownload) {
if (!_toInstallResources.contains(r)) {
_toInstallResources.add(r);
}
}
_toInstallResources.addAll(toDownload);
try {
// if any of our resources have already been marked valid this is not a
@@ -422,24 +437,20 @@ public abstract class Getdown extends Thread
throw new IOException("m.unable_to_repair");
} catch (Exception e) {
log.warning("getdown() failed.", e);
String msg = e.getMessage();
if (msg == null) {
msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
} else if (!msg.startsWith("m.")) {
// try to do something sensible based on the type of error
if (e instanceof FileNotFoundException) {
msg = MessageUtil.compose(
"m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
} else {
msg = MessageUtil.compose(
"m.init_error", MessageUtil.taint(msg), _ifc.installError);
}
// if we failed due to proxy errors, ask for proxy info
switch (_app.conn.state) {
case NEED_PROXY:
requestProxyInfo(false);
break;
case NEED_PROXY_AUTH:
requestProxyInfo(true);
break;
default:
log.warning("getdown() failed.", e);
fail(e);
_app.releaseLock();
break;
}
// Since we're dead, clear off the 'time remaining' label along with displaying the
// error message
fail(msg);
_app.releaseLock();
}
}
@@ -525,7 +536,7 @@ public abstract class Getdown extends Thread
// lastly regenerate the .jsa dump file that helps Java to start up faster
String vmpath = LaunchUtil.getJVMBinaryPath(javaLocalDir, false);
String[] command = new String[] { vmpath, "-Xshare:dump" };
String[] command = { vmpath, "-Xshare:dump" };
try {
log.info("Regenerating classes.jsa for " + vmpath + "...");
Runtime.getRuntime().exec(command);
@@ -620,7 +631,7 @@ public abstract class Getdown extends Thread
// create our user interface
createInterfaceAsync(false);
Downloader dl = new HTTPDownloader(_app.proxy) {
Downloader dl = new Downloader(_app.conn) {
@Override protected void resolvingDownloads () {
updateStatus("m.resolving");
}
@@ -732,7 +743,7 @@ public abstract class Getdown extends Thread
long minshow = _ifc.minShowSeconds * 1000L;
if (_container != null && uptime < minshow) {
try {
Thread.sleep(minshow - uptime);
TimeUnit.MILLISECONDS.sleep(minshow - uptime);
} catch (Exception e) {
}
}
@@ -848,8 +859,23 @@ public abstract class Getdown extends Thread
}
}
private void fail (Exception e) {
String msg = e.getMessage();
if (msg == null) {
msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
} else if (!msg.startsWith("m.")) {
// try to do something sensible based on the type of error
msg = MessageUtil.taint(msg);
msg = e instanceof FileNotFoundException ?
MessageUtil.compose("m.missing_resource", msg, _ifc.installError) :
MessageUtil.compose("m.init_error", msg, _ifc.installError);
}
// since we're dead, clear off the 'time remaining' label along with displaying the error
fail(msg);
}
/**
* Update the status to indicate getdown has failed for the reason in <code>message</code>.
* Update the status to indicate getdown has failed for the reason in {@code message}.
*/
protected void fail (String message)
{
@@ -930,14 +956,14 @@ public abstract class Getdown extends Thread
do {
URL url = _app.getTrackingProgressURL(++_reportedProgress);
if (url != null) {
new ProgressReporter(url).start();
reportProgress(url);
}
} while (_reportedProgress <= progress);
} else {
URL url = _app.getTrackingURL(event);
if (url != null) {
new ProgressReporter(url).start();
reportProgress(url);
}
}
}
@@ -1000,44 +1026,40 @@ public abstract class Getdown extends Thread
}
/** Used to fetch a progress report URL. */
protected class ProgressReporter extends Thread
{
public ProgressReporter (URL url) {
setDaemon(true);
_url = url;
}
@Override
public void run () {
try {
HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
// if we have a tracking cookie configured, configure the request with it
if (_app.getTrackingCookieName() != null &&
_app.getTrackingCookieProperty() != null) {
String val = System.getProperty(_app.getTrackingCookieProperty());
if (val != null) {
ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
}
}
// now request our tracking URL and ensure that we get a non-error response
ucon.connect();
protected void reportProgress (final URL url) {
Thread reporter = new Thread("Progress reporter") {
public void run () {
try {
if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
log.warning("Failed to report tracking event",
"url", _url, "rcode", ucon.getResponseCode());
HttpURLConnection ucon = _app.conn.openHttp(url, 0, 0);
// if we have a tracking cookie configured, configure the request with it
if (_app.getTrackingCookieName() != null &&
_app.getTrackingCookieProperty() != null) {
String val = System.getProperty(_app.getTrackingCookieProperty());
if (val != null) {
ucon.setRequestProperty(
"Cookie", _app.getTrackingCookieName() + "=" + val);
}
}
} finally {
ucon.disconnect();
// now request our tracking URL and ensure that we get a non-error response
ucon.connect();
try {
if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
log.warning("Failed to report tracking event",
"url", url, "rcode", ucon.getResponseCode());
}
} finally {
ucon.disconnect();
}
} catch (IOException ioe) {
log.warning("Failed to report tracking event", "url", url, "error", ioe);
}
} catch (IOException ioe) {
log.warning("Failed to report tracking event", "url", _url, "error", ioe);
}
}
protected URL _url;
};
reporter.setDaemon(true);
reporter.start();
}
/** Used to pass progress on to our user interface. */
@@ -101,7 +101,7 @@ public class GetdownApp
log.info("-- Cur dir: " + System.getProperty("user.dir"));
log.info("---------------------------------------------");
Getdown app = new Getdown(envc) {
Getdown getdown = new Getdown(envc) {
@Override
protected Container createContainer () {
// create our user interface, and display it
@@ -146,7 +146,7 @@ public class GetdownApp
}
if (_ifc.iconImages != null) {
ArrayList<Image> icons = new ArrayList<>();
List<Image> icons = new ArrayList<>();
for (String path : _ifc.iconImages) {
Image img = loadImage(path);
if (img == null) {
@@ -190,7 +190,7 @@ public class GetdownApp
String[] cmdarray;
if (LaunchUtil.isWindows()) {
String osName = System.getProperty("os.name", "");
if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) {
if (osName.contains("9") || osName.contains("Me")) {
cmdarray = new String[] {
"command.com", "/c", "start", "\"" + url + "\"" };
} else {
@@ -226,8 +226,7 @@ public class GetdownApp
// super.fail causes the UI to be created (if needed) on the next UI tick, so we
// want to wait until that happens before we attempt to redecorate the window
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
@Override public void run () {
// if the frame was set to be undecorated, make window decoration available
// to allow the user to close the window
if (_frame != null && _frame.isUndecorated()) {
@@ -247,7 +246,7 @@ public class GetdownApp
protected JFrame _frame;
};
app.start();
return app;
Getdown.run(getdown);
return getdown;
}
}
@@ -35,14 +35,16 @@ import static com.threerings.getdown.Log.log;
*/
public final class ProxyPanel extends JPanel implements ActionListener
{
public ProxyPanel (Getdown getdown, ResourceBundle msgs)
public ProxyPanel (Getdown getdown, ResourceBundle msgs, boolean updateAuth)
{
_getdown = getdown;
_msgs = msgs;
_updateAuth = updateAuth;
setLayout(new VGroupLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
add(new SaneLabelField(get("m.configure_proxy")));
String title = get(updateAuth ? "m.update_proxy_auth" : "m.configure_proxy");
add(new SaneLabelField(title));
add(new Spacer(5, 5));
JPanel row = new JPanel(new GridLayout());
@@ -61,19 +63,20 @@ public final class ProxyPanel extends JPanel implements ActionListener
row.add(new SaneLabelField(get("m.proxy_auth_required")), BorderLayout.WEST);
_useAuth = new JCheckBox();
row.add(_useAuth);
_useAuth.setSelected(updateAuth);
add(row);
row = new JPanel(new GridLayout());
row.add(new SaneLabelField(get("m.proxy_username")), BorderLayout.WEST);
_username = new SaneTextField();
_username.setEnabled(false);
_username.setEnabled(updateAuth);
row.add(_username);
add(row);
row = new JPanel(new GridLayout());
row.add(new SaneLabelField(get("m.proxy_password")), BorderLayout.WEST);
_password = new SanePasswordField();
_password.setEnabled(false);
_password.setEnabled(updateAuth);
row.add(_password);
add(row);
@@ -112,7 +115,13 @@ public final class ProxyPanel extends JPanel implements ActionListener
public void addNotify ()
{
super.addNotify();
_host.requestFocusInWindow();
if (_updateAuth) {
// we are asking the user to update the credentials for an existing proxy
// configuration, so focus that instead of the proxy host config
_username.requestFocusInWindow();
} else {
_host.requestFocusInWindow();
}
}
// documentation inherited
@@ -131,7 +140,7 @@ public final class ProxyPanel extends JPanel implements ActionListener
public void actionPerformed (ActionEvent e)
{
String cmd = e.getActionCommand();
if (cmd.equals("ok")) {
if ("ok".equals(cmd)) {
String user = null, pass = null;
if (_useAuth.isSelected()) {
user = _username.getText();
@@ -184,8 +193,9 @@ public final class ProxyPanel extends JPanel implements ActionListener
return dim;
}
protected Getdown _getdown;
protected ResourceBundle _msgs;
protected final Getdown _getdown;
protected final ResourceBundle _msgs;
protected final boolean _updateAuth;
protected JTextField _host;
protected JTextField _port;
@@ -8,31 +8,41 @@ package com.threerings.getdown.launcher;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.ServiceLoader;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import ca.beq.util.win32.registry.RegistryKey;
import ca.beq.util.win32.registry.RegistryValue;
import ca.beq.util.win32.registry.RootKey;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.net.Connector;
import com.threerings.getdown.spi.ProxyAuth;
import com.threerings.getdown.util.Config;
import com.threerings.getdown.util.ConnectionUtil;
import com.threerings.getdown.util.LaunchUtil;
import com.threerings.getdown.util.StringUtil;
import static com.threerings.getdown.Log.log;
public class ProxyUtil {
public final class ProxyUtil {
public static boolean autoDetectProxy (Application app)
{
@@ -57,25 +67,39 @@ public class ProxyUtil {
RegistryKey r = new RegistryKey(RootKey.HKEY_CURRENT_USER, PROXY_REGISTRY);
for (Iterator<?> iter = r.values(); iter.hasNext(); ) {
RegistryValue value = (RegistryValue)iter.next();
if (value.getName().equals("ProxyEnable")) {
enabled = value.getStringValue().equals("1");
if ("ProxyEnable".equals(value.getName())) {
enabled = "1".equals(value.getStringValue());
}
if (value.getName().equals("ProxyServer")) {
String strval = value.getStringValue();
int cidx = strval.indexOf(":");
if (cidx != -1) {
rport = strval.substring(cidx+1);
strval = strval.substring(0, cidx);
String[] hostPort = splitHostPort(value.getStringValue());
rhost = hostPort[0];
rport = hostPort[1];
}
if (value.getName().equals("AutoConfigURL")) {
String acurl = value.getStringValue();
Reader acjs = new InputStreamReader(new URL(acurl).openStream());
// technically we should be returning all this info and trying each proxy
// in succession, but that's complexity we'll leave for another day
for (String proxy : findPACProxiesForURL(acjs, app.getRemoteURL(""))) {
if (proxy.startsWith("PROXY ")) {
String[] hostPort = splitHostPort(proxy.substring(6));
rhost = hostPort[0];
rport = hostPort[1];
// TODO: is this valid? Does AutoConfigURL imply proxy enabled?
enabled = true;
break;
}
}
rhost = strval;
}
}
if (enabled) {
host = rhost;
port = rport;
} else {
log.info("Detected no proxy settings in the registry.");
}
} catch (Throwable t) {
log.info("Failed to find proxy settings in Windows registry", "error", t);
}
@@ -97,32 +121,31 @@ public class ProxyUtil {
return true;
}
public static boolean canLoadWithoutProxy (URL rurl)
public static boolean canLoadWithoutProxy (URL rurl, int timeoutSeconds)
{
log.info("Testing whether proxy is needed, via: " + rurl);
log.info("Attempting to fetch without proxy: " + rurl);
try {
// try to make a HEAD request for this URL (use short connect and read timeouts)
URLConnection conn = ConnectionUtil.open(Proxy.NO_PROXY, rurl, 5, 5);
if (conn instanceof HttpURLConnection) {
HttpURLConnection hcon = (HttpURLConnection)conn;
try {
hcon.setRequestMethod("HEAD");
hcon.connect();
// make sure we got a satisfactory response code
int rcode = hcon.getResponseCode();
if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
rcode == HttpURLConnection.HTTP_FORBIDDEN) {
log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
} else {
return true;
}
} finally {
hcon.disconnect();
}
} else {
// if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds);
// if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
if (!(conn instanceof HttpURLConnection)) {
return true;
}
// otherwise, try to make a HEAD request for this URL
HttpURLConnection hcon = (HttpURLConnection)conn;
try {
hcon.setRequestMethod("HEAD");
hcon.connect();
// make sure we got a satisfactory response code
int rcode = hcon.getResponseCode();
if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
rcode == HttpURLConnection.HTTP_FORBIDDEN) {
log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
} else {
return true;
}
} finally {
hcon.disconnect();
}
} catch (IOException ioe) {
log.info("Failed to HEAD " + rurl + ": " + ioe);
log.info("We probably need a proxy, but auto-detection failed.");
@@ -190,9 +213,14 @@ public class ProxyUtil {
}
boolean haveCreds = !StringUtil.isBlank(username) && !StringUtil.isBlank(password);
int pport = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
log.info("Using proxy", "host", host, "port", pport, "haveCreds", haveCreds);
app.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pport));
if (StringUtil.isBlank(host)) {
log.info("Using no proxy");
app.conn = new Connector(Proxy.NO_PROXY);
} else {
int pp = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
log.info("Using proxy", "host", host, "port", pp, "haveCreds", haveCreds);
app.conn = new Connector(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pp)));
}
if (haveCreds) {
final String fuser = username;
@@ -205,6 +233,54 @@ public class ProxyUtil {
}
}
public static class Resolver {
public String dnsResolve (String host) {
try {
return InetAddress.getByName(host).toString();
} catch (UnknownHostException uhe) {
return null;
}
}
}
public static String[] findPACProxiesForURL (Reader pac, URL url) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
Bindings globals = engine.createBindings();
globals.put("resolver", new Resolver());
engine.setBindings(globals, ScriptContext.GLOBAL_SCOPE);
try {
URL utils = ProxyUtil.class.getResource("PacUtils.js");
if (utils == null) {
log.error("Unable to load PacUtils.js");
return new String[0];
}
engine.eval(new InputStreamReader(utils.openStream()));
Object res = engine.eval(pac);
if (engine instanceof Invocable) {
Object[] args = new Object[] { url.toString(), url.getHost() };
res = ((Invocable) engine).invokeFunction("FindProxyForURL", args);
}
String[] proxies = res.toString().split(";");
for (int ii = 0; ii < proxies.length; ii += 1) {
proxies[ii] = proxies[ii].trim();
}
return proxies;
} catch (Exception e) {
log.warning("Failed to resolve PAC proxy", e);
}
return new String[0];
}
private static String[] splitHostPort (String hostPort) {
int cidx = hostPort.indexOf(":");
if (cidx == -1) {
return new String[] { hostPort, null};
} else {
return new String[] { hostPort.substring(0, cidx), hostPort.substring(cidx+1) };
}
}
protected static final String PROXY_REGISTRY =
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
}
@@ -14,7 +14,7 @@ public final class RotatingBackgrounds
{
public interface ImageLoader {
/** Loads and returns the image with the supplied path. */
public Image loadImage (String path);
Image loadImage (String path);
}
/**
@@ -35,7 +35,7 @@ public final class RotatingBackgrounds
}
/**
* Create a sequence of images to be rotated through from <code>backgrounds</code>.
* Create a sequence of images to be rotated through from {@code backgrounds}.
*
* Each String in backgrounds should be the path to the image, a semicolon, and the minimum
* amount of time to display the image in seconds. Each image will be active for an equal
@@ -26,12 +26,9 @@ import com.samskivert.swing.Label;
import com.samskivert.swing.LabelStyleConstants;
import com.samskivert.swing.util.SwingUtil;
import com.samskivert.util.Throttle;
import com.threerings.getdown.data.Application.UpdateInterface;
import com.threerings.getdown.util.MessageUtil;
import com.threerings.getdown.util.Rectangle;
import com.threerings.getdown.util.StringUtil;
import static com.threerings.getdown.Log.log;
/**
@@ -344,7 +341,7 @@ public final class StatusPanel extends JComponent
{
String msg = get(key);
if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
return key + String.valueOf(Arrays.asList(args));
return key + Arrays.asList(args);
}
/** Used by {@link #setStatus}, and {@link #setProgress}. */
@@ -0,0 +1,168 @@
/**
* Extracted from Mozilla Firefox 3.0.4, \mozilla\netwerk\base\src\nsProxyAutoConfig.js
*/
function dnsDomainIs (host, domain) {
return (host.length >= domain.length &&
host.substring(host.length - domain.length) == domain);
}
function dnsDomainLevels (host) {
return host.split('.').length-1;
}
function convert_addr (ipchars) {
var bytes = ipchars.split('.');
var result = (((bytes[0] & 0xff) << 24) |
((bytes[1] & 0xff) << 16) |
((bytes[2] & 0xff) << 8) |
( bytes[3] & 0xff ));
return result;
}
function dnsResolve (host) {
return resolver.dnsResolve(host)
}
function isInNet (addrOrHost, pattern, maskstr) {
var testRE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
var test = testRE.exec(addrOrHost);
if (test == null) {
addrOrHost = dnsResolve(addrOrHost);
if (addrOrHost == null) {
return false;
}
} else if (test[1] > 255 || test[2] > 255 || test[3] > 255 || test[4] > 255) {
return false; // not an IP address
}
var host = convert_addr(addrOrHost);
var pat = convert_addr(pattern);
var mask = convert_addr(maskstr);
return ((host & mask) == (pat & mask));
}
function isPlainHostName (host) {
return (host.search('\\.') == -1);
}
function isResolvable (host) {
var ip = dnsResolve(host);
return (ip != null);
}
function localHostOrDomainIs (host, hostdom) {
return (host == hostdom) || (hostdom.lastIndexOf(host + '.', 0) == 0);
}
function shExpMatch (url, pattern) {
pattern = pattern.replace(/\./g, '\\.');
pattern = pattern.replace(/\*/g, '.*');
pattern = pattern.replace(/\?/g, '.');
var newRe = new RegExp('^'+pattern+'$');
return newRe.test(url);
}
var wdays = {
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6
};
var months = {
JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11
};
function weekdayRange () {
function getDay (weekday) {
if (weekday in wdays) {
return wdays[weekday];
}
return -1;
}
var date = new Date();
var argc = arguments.length;
var wday;
if (argc < 1) {
return false;
}
if (arguments[argc - 1] == 'GMT') {
argc--;
wday = date.getUTCDay();
} else {
wday = date.getDay();
}
var wd1 = getDay(arguments[0]);
var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;
return (wd1 == -1 || wd2 == -1) ? false : (wd1 <= wday && wday <= wd2);
}
function dateRange () {
function getMonth (name) {
if (name in months) {
return months[name];
}
return -1;
}
var date = new Date();
var argc = arguments.length;
if (argc < 1) {
return false;
}
var isGMT = (arguments[argc - 1] == 'GMT');
if (isGMT) {
argc--;
}
// function will work even without explict handling of this case
if (argc == 1) {
var tmp = parseInt(arguments[0]);
if (isNaN(tmp)) {
return ((isGMT ? date.getUTCMonth() : date.getMonth()) == getMonth(arguments[0]));
} else if (tmp < 32) {
return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp);
} else {
return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == tmp);
}
}
var year = date.getFullYear();
var date1, date2;
date1 = new Date(year, 0, 1, 0, 0, 0);
date2 = new Date(year, 11, 31, 23, 59, 59);
var adjustMonth = false;
for (var i = 0; i < (argc >> 1); i++) {
var tmp = parseInt(arguments[i]);
if (isNaN(tmp)) {
var mon = getMonth(arguments[i]);
date1.setMonth(mon);
} else if (tmp < 32) {
adjustMonth = (argc <= 2);
date1.setDate(tmp);
} else {
date1.setFullYear(tmp);
}
}
for (var i = (argc >> 1); i < argc; i++) {
var tmp = parseInt(arguments[i]);
if (isNaN(tmp)) {
var mon = getMonth(arguments[i]);
date2.setMonth(mon);
} else if(tmp < 32) {
date2.setDate(tmp);
} else {
date2.setFullYear(tmp);
}
}
if (adjustMonth) {
date1.setMonth(date.getMonth());
date2.setMonth(date.getMonth());
}
if (isGMT) {
var tmp = date;
tmp.setFullYear(date.getUTCFullYear());
tmp.setMonth(date.getUTCMonth());
tmp.setDate(date.getUTCDate());
tmp.setHours(date.getUTCHours());
tmp.setMinutes(date.getUTCMinutes());
tmp.setSeconds(date.getUTCSeconds());
date = tmp;
}
return ((date1 <= date) && (date <= date2));
}
@@ -17,6 +17,9 @@ m.configure_proxy = <html>We were unable to connect to the application server to
<p> Your computer may access the Internet through a proxy and we were unable to automatically \
detect your proxy settings. If you know your proxy settings, you can enter them below.</html>
m.update_proxy_auth = <html>The stored proxy user/password is wrong or obsolete. \
<p>Please provide an updated user/password combination.</html>
m.proxy_extra = <html>If you are sure that you don't use a proxy then \
perhaps there is a temporary Internet outage that is preventing us from \
communicating with the servers. In this case, you can cancel and try \
@@ -16,6 +16,9 @@ m.configure_proxy = <html>Es konnte keine Verbindung zum Applikations-Server auf
Wenn kein Proxy verwendet werden soll, löschen Sie bitte alle Einträge in den unten \
stehenden Feldern und klicken sie auf OK.</html>
m.update_proxy_auth = <html>Gespeicherte Proxy User/Passwort Kombination ungültig oder obsolet. \
<p>Bitte geben Sie die korrekte User/Passwort Kombination ein.</html>
m.proxy_extra = <html>Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \
an Ihren Administrator.</html>
@@ -20,7 +20,11 @@ m.configure_proxy = <html>Impossibile collegarsi al server per \
questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \
tue impostazioni del proxy, puoi inserirle di seguito.</html>
m.proxy_extra = <html>Se sei sicuro di non usare proxy \
m.update_proxy_auth = <html>Combinazione User/Password salvata per il proxy non è valida \
oppure obsoleta. \
<p>Perfavore inserire una combinazione User/Password valida.</html>
m.proxy_extra = <html>Se sei sicuro di non usare proxy \
potrebbe essere un problema di internet o di collegamento con il server. \
In questo caso puoi annullare e ripetere l'installazione più tardi.</html>
@@ -0,0 +1,137 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.launcher;
import java.io.StringReader;
import java.net.URL;
import org.junit.Test;
import static org.junit.Assert.*;
public class ProxyUtilTest {
static String[] strs (String... strs) { return strs; }
static String[] findPAC (String code, String url) throws Exception {
return ProxyUtil.findPACProxiesForURL(new StringReader(code), new URL(url));
}
static void testPAC (String code, String url, String... expectedProxies) throws Exception {
assertArrayEquals(expectedProxies, findPAC(code, url));
}
@Test public void testPACProxy () throws Exception {
String EXAMPLE0 =
"function FindProxyForURL(url, host) {\n" +
" if (shExpMatch(host, '*.example.com')) { return 'DIRECT'; }\n" +
" if (isInNet(host, '10.0.0.0', '255.255.248.0')) {\n" +
" return 'PROXY fastproxy.example.com:8080';\n" +
" }\n" +
" return 'PROXY proxy.example.com:8080; DIRECT';\n" +
"}\n";
testPAC(EXAMPLE0, "http://test.example.com/", "DIRECT");
testPAC(EXAMPLE0, "http://10.0.1.1/", "PROXY fastproxy.example.com:8080");
testPAC(EXAMPLE0, "http://chicken.gov/", "PROXY proxy.example.com:8080", "DIRECT");
String EXAMPLE1 =
"function FindProxyForURL(url, host) {" +
" if (isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) {" +
" return 'DIRECT';" +
" } else {" +
" return 'PROXY w3proxy.mozilla.org:8080; DIRECT';" +
" }" +
"}";
testPAC(EXAMPLE1, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE1, "http://www.mozilla.org/", "DIRECT");
testPAC(EXAMPLE1, "http://foo.mozilla.org/", "DIRECT");
testPAC(EXAMPLE1, "http://localhost/", "DIRECT");
String EXAMPLE2 =
" function FindProxyForURL(url, host) {\n" +
" if ((isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) &&\n" +
" !localHostOrDomainIs(host, 'www.mozilla.org') &&\n" +
" !localHostOrDomainIs(host, 'merchant.mozilla.org')) {\n" +
" return 'DIRECT';\n" +
" } else {\n" +
" return 'PROXY w3proxy.mozilla.org:8080; DIRECT';\n" +
" }\n" +
" }";
testPAC(EXAMPLE2, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE2, "http://www.mozilla.org/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE2, "http://www/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE2, "http://foo.mozilla.org/", "DIRECT");
testPAC(EXAMPLE2, "http://localhost/", "DIRECT");
String EXAMPLE3A =
"function FindProxyForURL(url, host) {\n" +
" if (isResolvable(host)) return 'DIRECT';\n" +
" else return 'PROXY proxy.mydomain.com:8080';\n" +
"}";
testPAC(EXAMPLE3A, "http://www.mozilla.org/", "DIRECT");
testPAC(EXAMPLE3A, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080");
String EXAMPLE3B =
"function FindProxyForURL(url, host) {\n" +
" if (isPlainHostName(host) ||\n" +
" dnsDomainIs(host, '.mydomain.com') ||\n" +
" isResolvable(host)) {\n" +
" return 'DIRECT';\n" +
" } else {\n" +
" return 'PROXY proxy.mydomain.com:8080';\n" +
" }\n" +
"}";
testPAC(EXAMPLE3B, "http://plain/", "DIRECT");
testPAC(EXAMPLE3B, "http://foo.mydomain.com/", "DIRECT");
testPAC(EXAMPLE3B, "http://www.mozilla.org/", "DIRECT");
testPAC(EXAMPLE3B, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080");
// example 4
// function FindProxyForURL(url, host) {
// if (isInNet(host, '198.95.0.0', '255.255.0.0')) return 'DIRECT';
// else return 'PROXY proxy.mydomain.com:8080';
// }
// function FindProxyForURL(url, host) {
// if (isPlainHostName(host) ||
// dnsDomainIs(host, '.mydomain.com') ||
// isInNet(host, '198.95.0.0', '255.255.0.0')) {
// return 'DIRECT';
// } else {
// return 'PROXY proxy.mydomain.com:8080';
// }
// }
// example 5
// function FindProxyForURL(url, host) {
// if (isPlainHostName(host) || dnsDomainIs(host, '.mydomain.com')) return 'DIRECT';
// else if (shExpMatch(host, '*.com')) return 'PROXY proxy1.mydomain.com:8080; ' +
// 'PROXY proxy4.mydomain.com:8080';
// else if (shExpMatch(host, '*.edu')) return 'PROXY proxy2.mydomain.com:8080; ' +
// 'PROXY proxy4.mydomain.com:8080';
// else return 'PROXY proxy3.mydomain.com:8080; ' +
// 'PROXY proxy4.mydomain.com:8080';
// }
// example 6
// function FindProxyForURL(url, host) {
// if (url.substring(0, 5) == 'http:') {
// return 'PROXY http-proxy.mydomain.com:8080';
// }
// else if (url.substring(0, 4) == 'ftp:') {
// return 'PROXY ftp-proxy.mydomain.com:8080';
// }
// else if (url.substring(0, 7) == 'gopher:') {
// return 'PROXY gopher-proxy.mydomain.com:8080';
// }
// else if (url.substring(0, 6) == 'https:' ||
// url.substring(0, 6) == 'snews:') {
// return 'PROXY security-proxy.mydomain.com:8080';
// } else {
// return 'DIRECT';
// }
// }
}
}