diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c9100..c0b2310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 726d6bf..a1deca4 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -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 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; } diff --git a/core/src/main/java/com/threerings/getdown/data/SysProps.java b/core/src/main/java/com/threerings/getdown/data/SysProps.java index d89bf70..1de9cd4 100644 --- a/core/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/core/src/main/java/com/threerings/getdown/data/SysProps.java @@ -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 diff --git a/core/src/main/java/com/threerings/getdown/net/Connector.java b/core/src/main/java/com/threerings/getdown/net/Connector.java new file mode 100644 index 0000000..9e74787 --- /dev/null +++ b/core/src/main/java/com/threerings/getdown/net/Connector.java @@ -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 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); + } + } +} diff --git a/core/src/main/java/com/threerings/getdown/net/Downloader.java b/core/src/main/java/com/threerings/getdown/net/Downloader.java index 6033e2f..505958a 100644 --- a/core/src/main/java/com/threerings/getdown/net/Downloader.java +++ b/core/src/main/java/com/threerings/getdown/net/Downloader.java @@ -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 _sizes = new HashMap<>(); + protected final Map _sizes = new HashMap<>(); /** The bytes downloaded for each resource. */ - protected Map _downloaded = new HashMap<>(); + protected final Map _downloaded = new HashMap<>(); /** The time at which the file transfer began. */ protected long _start; diff --git a/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java deleted file mode 100644 index a7a3287..0000000 --- a/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java +++ /dev/null @@ -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; -} diff --git a/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java deleted file mode 100644 index a8e5361..0000000 --- a/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java +++ /dev/null @@ -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); - } -} diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java index 0aa1522..09d2d6e 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -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. */ diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index 7de56e4..e4e3320 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java @@ -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; } } diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java index 40934f7..5f18896 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java @@ -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; diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java index 5616f16..209c7bb 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java @@ -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; diff --git a/launcher/src/main/resources/com/threerings/getdown/messages.properties b/launcher/src/main/resources/com/threerings/getdown/messages.properties index 92e13ee..b113c8e 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages.properties @@ -17,6 +17,9 @@ m.configure_proxy = We were unable to connect to the application server to

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. +m.update_proxy_auth = The stored proxy user/password is wrong or obsolete. \ +

Please provide an updated user/password combination. + m.proxy_extra = 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 \ diff --git a/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/launcher/src/main/resources/com/threerings/getdown/messages_de.properties index b013f15..1c3af16 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages_de.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages_de.properties @@ -16,6 +16,9 @@ m.configure_proxy = 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. +m.update_proxy_auth = Gespeicherte Proxy User/Passwort Kombination ungültig oder obsolet. \ +

Bitte geben Sie die korrekte User/Passwort Kombination ein. + m.proxy_extra = Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \ an Ihren Administrator. diff --git a/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/launcher/src/main/resources/com/threerings/getdown/messages_it.properties index 1328650..c2481ea 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages_it.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages_it.properties @@ -20,7 +20,11 @@ m.configure_proxy = Impossibile collegarsi al server per \ questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \ tue impostazioni del proxy, puoi inserirle di seguito. -m.proxy_extra = Se sei sicuro di non usare proxy \ +m.update_proxy_auth = Combinazione User/Password salvata per il proxy non è valida \ + oppure obsoleta. \ +

Perfavore inserire una combinazione User/Password valida. + +m.proxy_extra = 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.