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
@@ -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>