diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 2ae58a39844e5db7eb7ea899867807f3897e35c7..9abcdd0fd61162a12d0b044db7a6aba9d60de0e9 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -24,6 +24,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.client.streaming.I2PSocketManager;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
+import net.i2p.util.EepGet;
 import net.i2p.util.EventDispatcher;
 import net.i2p.util.InternalSocket;
 import net.i2p.util.Log;
@@ -409,60 +410,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
      *  @since 0.9.4
      */
     private static Map<String, String> parseArgs(String args) {
-        Map<String, String> rv = new HashMap<String, String>(8);
-        char data[] = args.toCharArray();
-        StringBuilder buf = new StringBuilder(32);
-        boolean isQuoted = false;
-        String key = null;
-        for (int i = 0; i < data.length; i++) {
-            switch (data[i]) {
-                case '\"':
-                    if (isQuoted) {
-                        // keys never quoted
-                        if (key != null) {
-                            rv.put(key, buf.toString().trim());
-                            key = null;
-                        }
-                        buf.setLength(0);
-                    }
-                    isQuoted = !isQuoted;
-                    break;
-
-                case ' ':
-                case '\r':
-                case '\n':
-                case '\t':
-                case ',':
-                    // whitespace - if we're in a quoted section, keep this as part of the quote,
-                    // otherwise use it as a delim
-                    if (isQuoted) {
-                        buf.append(data[i]);
-                    } else {
-                        if (key != null) {
-                            rv.put(key, buf.toString().trim());
-                            key = null;
-                        }
-                        buf.setLength(0);
-                    }
-                    break;
-
-                case '=':
-                    if (isQuoted) {
-                        buf.append(data[i]);
-                    } else {
-                        key = buf.toString().trim().toLowerCase(Locale.US);
-                        buf.setLength(0);
-                    }
-                    break;
-
-                default:
-                    buf.append(data[i]);
-                    break;
-            }
-        }
-        if (key != null)
-            rv.put(key, buf.toString().trim());
-        return rv;
+        // moved to EepGet, since it needs this too
+        return EepGet.parseAuthArgs(args);
     }
 
     //////// Error page stuff
diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java
index 72f6391acc1f09b9159b99058274a3a9cb60e6b2..1a6e23a011fe84a73823de2263cc44ef6e3a3056 100644
--- a/core/java/src/net/i2p/util/EepGet.java
+++ b/core/java/src/net/i2p/util/EepGet.java
@@ -19,12 +19,15 @@ import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.Formatter;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 
 import gnu.getopt.Getopt;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
@@ -87,6 +90,10 @@ public class EepGet {
     protected boolean _isGzippedResponse;
     protected IOException _decompressException;
 
+    // following for proxy digest auth
+    // only created via addAuthorization()
+    private AuthState _authState;
+
     /** this will be replaced by the HTTP Proxy if we are using it */
     protected static final String USER_AGENT = "Wget/1.11.4";
     protected static final long CONNECT_TIMEOUT = 45*1000;
@@ -662,6 +669,7 @@ public class EepGet {
         }
         
         if (_redirectLocation != null) {
+            // we also are here after a 407
             //try {
                 if (_redirectLocation.startsWith("http://")) {
                     _actualURL = _redirectLocation;
@@ -680,10 +688,27 @@ public class EepGet {
             //} catch (MalformedURLException mue) {
             //    throw new IOException("Redirected from an invalid URL");
             //}
-            _redirects++;
-            if (_redirects > 5)
-                throw new IOException("Too many redirects: to " + _redirectLocation);
-            if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation);
+
+            AuthState as = _authState;
+            if (_responseCode == 407) {
+                if (!_shouldProxy)
+                    throw new IOException("Proxy auth response from non-proxy");
+                if (as == null)
+                    throw new IOException("Proxy requires authentication");
+                if (as.authSent)
+                    throw new IOException("Proxy authentication failed");  // ignore stale
+                if (as.authChallenge == null)
+                    throw new IOException("Bad proxy auth response");
+                if (_log.shouldLog(Log.INFO)) _log.info("Adding auth");
+                // actually happens in getRequest()
+            } else {
+                _redirects++;
+                if (_redirects > 5)
+                    throw new IOException("Too many redirects: to " + _redirectLocation);
+                if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation);
+                if (as != null)
+                    as.authSent = false;
+            }
 
             // reset some important variables, we don't want to save the values from the redirect
             _bytesRemaining = -1;
@@ -871,6 +896,7 @@ public class EepGet {
                 _keepFetching = false;
                 _notModified = true;
                 return; 
+            case 401: // server auth
             case 403: // bad req
             case 404: // not found
             case 409: // bad addr helper
@@ -889,6 +915,17 @@ public class EepGet {
                         _out = new FileOutputStream(_outputFile, true);
                 }
                 break;
+            case 407: // proxy auth
+                // we will treat this is a redirect if we haven't sent auth yet
+                //_redirectLocation will be set to _actualURL below
+                _alreadyTransferred = 0;
+                if (_authState != null)
+                    rcOk = !_authState.authSent;
+                else
+                    rcOk = false;
+                redirect = rcOk;
+                _keepFetching = rcOk;
+                break;
             case 416: // completed (or range out of reach)
                 _bytesRemaining = 0;
                 if (_alreadyTransferred > 0 || !_shouldWriteErrorToOutput) {
@@ -970,11 +1007,14 @@ public class EepGet {
                     increment(lookahead, cur);
                     if (isEndOfHeaders(lookahead)) {
                         if (!rcOk)
-                            throw new IOException("Invalid HTTP response code: " + _responseCode);
+                            throw new IOException("Invalid HTTP response code: " + _responseCode + ' ' + _responseText);
                         if (_encodingChunked) {
                             _bytesRemaining = readChunkLength();
                         }
-                        if (!redirect) _redirectLocation = null;
+                        if (!redirect)
+                            _redirectLocation = null;
+                        else if (_responseCode == 407)
+                            _redirectLocation = _actualURL;
                         return;
                     }
                     break;
@@ -1081,6 +1121,8 @@ public class EepGet {
             _contentType=val;
         } else if (key.equals("location")) {
             _redirectLocation=val;
+        } else if (key.equals("proxy-authenticate") && _responseCode == 407 && _authState != null && _shouldProxy) {
+            _authState.setAuthChallenge(val);
         } else {
             // ignore the rest
         }
@@ -1192,10 +1234,11 @@ public class EepGet {
                 urlToSend += '?' + query;
         }
         if (post) {
-            buf.append("POST ").append(urlToSend).append(" HTTP/1.1\r\n");
+            buf.append("POST ");
         } else {
-            buf.append("GET ").append(urlToSend).append(" HTTP/1.1\r\n");
+            buf.append("GET ");
         }
+        buf.append(urlToSend).append(" HTTP/1.1\r\n");
         // RFC 2616 sec 5.1.2 - host + port (NOT authority, which includes userinfo)
         buf.append("Host: ").append(host);
         if (port >= 0)
@@ -1240,6 +1283,12 @@ public class EepGet {
         }
         if(!uaOverridden)
             buf.append("User-Agent: " + USER_AGENT + "\r\n");
+        if (_authState != null && _shouldProxy && _authState.authMode != AUTH_MODE.NONE) {
+            buf.append("Proxy-Authorization: ");
+            String method = post ? "POST" : "GET";
+            buf.append(_authState.getAuthHeader(method, urlToSend));
+            buf.append("\r\n");
+        }
         buf.append("Connection: close\r\n\r\n");
         if (post)
             buf.append(_postData);
@@ -1335,9 +1384,240 @@ public class EepGet {
      *  @since 0.8.9
      */
     public void addAuthorization(String userName, String password) {
-        if (_shouldProxy)
-            addHeader("Proxy-Authorization", 
-                      "Basic " + Base64.encode(DataHelper.getUTF8(userName + ':' + password), true));  // true = use standard alphabet
+        if (_shouldProxy) {
+            // Could only do this for Basic
+            // Now we always wait for the 407, in the hope we can use Digest
+            //addHeader("Proxy-Authorization", 
+            //          "Basic " + Base64.encode(DataHelper.getUTF8(userName + ':' + password), true));  // true = use standard alphabet
+            if (_authState != null)
+                throw new IllegalStateException();
+            _authState = new AuthState(userName, password);
+        }
+    }
+
+    /**
+     *  Parse the args in an authentication header.
+     *
+     *  Modified from LoadClientAppsJob.
+     *  All keys are mapped to lower case.
+     *  Double quotes around values are stripped.
+     *  Ref: RFC 2617
+     *
+     *  Public for I2PTunnelHTTPClientBase; use outside of tree at own risk, subject to change or removal
+     *
+     *  @param args non-null, starting after "Digest " or "Basic "
+     *  @since 0.9.4, moved from I2PTunnelHTTPClientBase in 0.9.12
+     */
+    public static Map<String, String> parseAuthArgs(String args) {
+        Map<String, String> rv = new HashMap<String, String>(8);
+        char data[] = args.toCharArray();
+        StringBuilder buf = new StringBuilder(32);
+        boolean isQuoted = false;
+        String key = null;
+        for (int i = 0; i < data.length; i++) {
+            switch (data[i]) {
+                case '\"':
+                    if (isQuoted) {
+                        // keys never quoted
+                        if (key != null) {
+                            rv.put(key, buf.toString().trim());
+                            key = null;
+                        }
+                        buf.setLength(0);
+                    }
+                    isQuoted = !isQuoted;
+                    break;
+
+                case ' ':
+                case '\r':
+                case '\n':
+                case '\t':
+                case ',':
+                    // whitespace - if we're in a quoted section, keep this as part of the quote,
+                    // otherwise use it as a delim
+                    if (isQuoted) {
+                        buf.append(data[i]);
+                    } else {
+                        if (key != null) {
+                            rv.put(key, buf.toString().trim());
+                            key = null;
+                        }
+                        buf.setLength(0);
+                    }
+                    break;
+
+                case '=':
+                    if (isQuoted) {
+                        buf.append(data[i]);
+                    } else {
+                        key = buf.toString().trim().toLowerCase(Locale.US);
+                        buf.setLength(0);
+                    }
+                    break;
+
+                default:
+                    buf.append(data[i]);
+                    break;
+            }
+        }
+        if (key != null)
+            rv.put(key, buf.toString().trim());
+        return rv;
+    }
+
+
+    /**
+     *  @since 0.9.12
+     */
+    private enum AUTH_MODE {NONE, BASIC, DIGEST, UNKNOWN}
+
+    /**
+     *  Manage the authentication parameters
+     *  Ref: RFC 2617
+     *  Supports both Basic and Digest, however i2ptunnel HTTP proxy
+     *  has migrated all previous Basic support to Digest.
+     *
+     *  @since 0.9.12
+     */
+    private class AuthState {
+        private final String username;
+        private final String password;
+        // as recvd in 407
+        public AUTH_MODE authMode = AUTH_MODE.NONE;
+        // as recvd in 407, after the mode string
+        private String authChallenge;
+        public boolean authSent;
+        private int nonceCount;
+        private String cnonce;
+        // as parsed from authChallenge
+        private Map<String, String> args;
+
+        public AuthState(String user, String pw) {
+            username = user;
+            password = pw;
+        }
+
+        /**
+         *  May be called multiple times, save the best one
+         */
+        public void setAuthChallenge(String auth) {
+            String authLC = auth.toLowerCase(Locale.US);
+            if (authLC.startsWith("basic ")) {
+                // better than anything but DIGEST
+                if (authMode != AUTH_MODE.DIGEST) {
+                    // use standard alphabet
+                    authMode = AUTH_MODE.BASIC;
+                    authChallenge = auth.substring(6);
+                }
+            } else if (authLC.startsWith("digest ")) {
+                // better than anything
+                authMode = AUTH_MODE.DIGEST;
+                authChallenge = auth.substring(7);
+            } else {
+                // better than NONE only
+                if (authMode == AUTH_MODE.NONE) {
+                    authMode = AUTH_MODE.UNKNOWN;
+                    authChallenge = "";
+                }
+            }
+            nonceCount = 0;
+            args = null;
+        }
+
+        public String getAuthHeader(String method, String uri) throws IOException {
+            switch (authMode) {
+                case BASIC:
+                    authSent = true;
+                    // use standard alphabet
+                    return "Basic " +
+                           Base64.encode(DataHelper.getUTF8(username + ':' + password), true);
+
+                case DIGEST:
+                    if (args == null)
+                        args = parseAuthArgs(authChallenge);
+                    Map<String, String> outArgs = generateAuthArgs(method, uri);
+                    if (outArgs == null)
+                        throw new IOException("Bad proxy auth response");
+                    StringBuilder buf = new StringBuilder(256);
+                    buf.append("Digest");
+                    for (Map.Entry<String, String> e : outArgs.entrySet()) {
+                        buf.append(' ').append(e.getKey()).append('=').append(e.getValue());
+                    }
+                    authSent = true;
+                    return buf.toString();
+
+                default:
+                    throw new IOException("Unknown proxy auth type " + authChallenge);
+            }
+        }
+
+        /**
+         *  Generate the digest authentication parameters
+         *  Ref: RFC 2617
+         *
+         *  @since 0.9.12 modified from I2PTunnelHTTPClientBase.validateDigest()
+         */
+        public Map<String, String> generateAuthArgs(String method, String uri) throws IOException {
+            Map<String, String> rv = new HashMap<String, String>(12);
+            String realm = args.get("realm");
+            String nonce = args.get("nonce");
+            String qop = args.get("qop");
+            String opaque = args.get("opaque");
+            //String algorithm = args.get("algorithm");
+            //String stale = args.get("stale");
+            if (realm == null || nonce == null) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Bad digest request: " + DataHelper.toString(args));
+                throw new IOException("Bad auth response");
+            }
+            rv.put("username", '"' + username + '"');
+            rv.put("realm", '"' + realm + '"');
+            rv.put("nonce", '"' + nonce + '"');
+            rv.put("uri", '"' + uri + '"');
+            if (opaque != null)
+                rv.put("opaque", '"' + opaque + '"');
+            String kdMiddle;
+            if ("auth".equals(qop)) {
+                rv.put("qop", "\"auth\"");
+                if (cnonce == null) {
+                    byte[] rand = new byte[5];
+                    _context.random().nextBytes(rand);
+                    cnonce = Base32.encode(rand);
+                }  // else reuse on redirect
+                rv.put("cnonce", '"' + cnonce + '"');
+                String nc = lc8hex(++nonceCount);
+                rv.put("nc", nc);
+                kdMiddle = ':' + nc + ':' + cnonce + ':' + qop;
+            } else {
+                kdMiddle = "";
+            }
+
+            // get H(A1)
+            String ha1 = PasswordManager.md5Hex(username + ':' + realm + ':' + password);
+            // get H(A2)
+            String a2 = method + ':' + uri;
+            String ha2 = PasswordManager.md5Hex(a2);
+            // response
+            String kd = ha1 + ':' + nonce + kdMiddle + ':' + ha2;
+            rv.put("response", '"' + PasswordManager.md5Hex(kd) + '"');
+            return rv;
+        }
+    }
+
+    /**
+     *  @return 8 hex chars, lower case, e.g. 00000001
+     *  @since 0.8.10
+     */
+    private static String lc8hex(int nc) {
+        StringBuilder buf = new StringBuilder(8);
+        for (int i = 28; i >= 0; i -= 4) {
+            int v = (nc >> i) & 0xf;
+            if (v < 10)
+                buf.append((char) (v + '0'));
+            else
+                buf.append((char) (v + 'a' - 10));
+        }
+        return buf.toString();
     }
 
     /**