Proxy handling now detects need for updated credentials.

This revamps the way proxy handling is done to allow us to just proceed as
normal, and then if we get HTTP errors that indicate that we need a proxy or
need proxy credentials, we ask for them and then retry everything.

We still try to auto-detect the need for a proxy on our very first invocation
because on the first invocation, a failure to fetch a URL may well indicate
that a proxy is needed, but that same assumption does not hold on subsequent
invocations. In those later cases, it's probably just a transient network
failure and it would be weird and annoying to pop up the "please provide proxy
config" dialog in those cases.

Thanks pb00068 for getting this ball rolling, even though once again I have
nearly entirely rewritten the PR. I need to kick that habit. :)
This commit is contained in:
Michael Bayne
2019-05-28 17:21:59 -07:00
parent e337ccb743
commit 0a4bfe65c4
14 changed files with 481 additions and 380 deletions
+8
View File
@@ -10,6 +10,14 @@
* 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,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;
@@ -271,10 +270,10 @@ public class Application
return config;
}
/** 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
/** 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}
@@ -989,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");
@@ -1258,10 +1249,10 @@ 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))) {
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) {
@@ -1485,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);
}
@@ -1628,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");
@@ -1643,26 +1632,7 @@ public class Application
}
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;
}
@@ -101,6 +101,16 @@ public final 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;
}
@@ -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 final 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);
}
}
@@ -39,17 +39,16 @@ import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.getdown.data.Application;
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.Config;
import com.threerings.getdown.util.ConnectionUtil;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.LaunchUtil;
import com.threerings.getdown.util.MessageUtil;
@@ -62,12 +61,22 @@ 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.
@@ -134,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
@@ -156,59 +185,20 @@ public abstract class Getdown extends Thread
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);
fail(e);
}
}
/**
* 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 {
@@ -217,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;
}
@@ -234,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.
@@ -264,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) {
@@ -279,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 " +
@@ -424,9 +437,20 @@ public abstract class Getdown extends Thread
throw new IOException("m.unable_to_repair");
} catch (Exception e) {
log.warning("getdown() failed.", e);
fail(e);
_app.releaseLock();
// 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;
}
}
}
@@ -607,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");
}
@@ -841,11 +865,12 @@ public abstract class Getdown extends Thread
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 = e instanceof FileNotFoundException
? MessageUtil.compose("m.missing_resource", MessageUtil.taint(msg), _ifc.installError)
: MessageUtil.compose("m.init_error", MessageUtil.taint(msg), _ifc.installError);
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 message
// since we're dead, clear off the 'time remaining' label along with displaying the error
fail(msg);
}
@@ -931,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);
}
}
}
@@ -1001,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
@@ -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
@@ -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;
@@ -34,9 +34,9 @@ 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;
@@ -121,32 +121,31 @@ public final 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.");
@@ -214,9 +213,14 @@ public final 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;
@@ -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>