Last active
September 17, 2017 10:05
-
-
Save basinilya/26f07a1b945423f832674f419ce08d29 to your computer and use it in GitHub Desktop.
ConcurrentAuthenticator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.foo.concurrentauthenticator; | |
import java.net.Authenticator; | |
import java.net.PasswordAuthentication; | |
import java.util.concurrent.Callable; | |
import java.util.concurrent.ExecutionException; | |
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.Future; | |
import java.util.concurrent.ThreadFactory; | |
/** | |
* java.net.Authenticator disallows simultaneous password prompting from different threads, because | |
* the callback runs inside a synchronized block. This implementation circumvents it by calling | |
* {@link Object#wait()} and losing the monitor ownership. The side effect is that the actual | |
* password prompt has to run in a separate thread. | |
*/ | |
public abstract class ConcurrentAuthenticator extends Authenticator implements Cloneable { | |
protected abstract PasswordAuthentication getPasswordAuthentication(final Thread callerThread); | |
private final ExecutorService executor; | |
public ConcurrentAuthenticator() { | |
this(mkExecutor()); | |
} | |
protected static ExecutorService mkExecutor() { | |
return Executors.newCachedThreadPool(new ThreadFactory() { | |
final ThreadFactory parent = Executors.defaultThreadFactory(); | |
@Override | |
public Thread newThread(final Runnable r) { | |
final Thread res = parent.newThread(r); | |
res.setDaemon(true); | |
return res; | |
} | |
}); | |
} | |
public ConcurrentAuthenticator(final ExecutorService executor) { | |
this.executor = executor; | |
} | |
protected PasswordAuthentication unlockAndGetPasswordAuthentication(final Thread callerThread, | |
final ConcurrentAuthenticator selfCopy) { | |
// On android-19 it's static synchronized or no synchronized at all | |
final Object lock = chooseLock(0, ConcurrentAuthenticator.this, Authenticator.class); | |
try { | |
class Task implements Callable<PasswordAuthentication> { | |
boolean done; | |
@Override | |
public PasswordAuthentication call() throws Exception { | |
try { | |
return selfCopy.getPasswordAuthentication(callerThread); | |
} finally { | |
synchronized (lock) { | |
done = true; | |
lock.notifyAll(); | |
} | |
} | |
} | |
} | |
final Task task = new Task(); | |
final Future<PasswordAuthentication> fut = executor.submit(task); | |
synchronized (lock) { | |
while (!task.done) { | |
lock.wait(); | |
} | |
} | |
return fut.get(); | |
} catch (final InterruptedException e) { | |
Thread.currentThread().interrupt(); | |
throw new RuntimeException(e); | |
} catch (final ExecutionException e) { | |
throw toRTE(e.getCause()); | |
} | |
} | |
private static RuntimeException toRTE(final Throwable cause) { | |
if (cause instanceof RuntimeException) { | |
return (RuntimeException) cause; | |
} | |
if (cause instanceof Exception) { | |
return new RuntimeException(cause); | |
} | |
if (cause instanceof Error) { | |
throw (Error) cause; | |
} | |
throw new Error(cause); | |
} | |
private static Object chooseLock(final int index, final Object... candidates) { | |
if (index >= candidates.length) { | |
return candidates[0]; | |
} | |
try { | |
final Object o = candidates[index]; | |
o.notify(); | |
return o; | |
} catch (final IllegalMonitorStateException e) { | |
return chooseLock(index + 1, candidates); | |
} | |
} | |
@Override | |
protected final PasswordAuthentication getPasswordAuthentication() { | |
try { | |
final ConcurrentAuthenticator selfCopy = (ConcurrentAuthenticator) clone(); | |
return unlockAndGetPasswordAuthentication(Thread.currentThread(), selfCopy); | |
} catch (final CloneNotSupportedException e) { | |
throw new RuntimeException("this can't be", e); | |
} | |
} | |
@Override | |
public String toString() { | |
return "A [requestingHost=" + getRequestingHost() + ", requestingSite=" | |
+ getRequestingSite() + ", requestingPort=" + getRequestingPort() | |
+ ", requestingProtocol=" + getRequestingProtocol() + ", requestingPrompt=" | |
+ getRequestingPrompt() + ", requestingScheme=" + getRequestingScheme() | |
+ ", requestingURL=" + getRequestingURL() + ", requestorType=" + getRequestorType() | |
+ "]"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.foo.concurrentauthenticator; | |
import java.io.InputStream; | |
import java.net.PasswordAuthentication; | |
import java.net.URL; | |
import java.util.concurrent.Callable; | |
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.Future; | |
import java.util.concurrent.ThreadPoolExecutor; | |
import java.util.concurrent.TimeUnit; | |
public class ConcurrentAuthenticatorTest { | |
public static void main(final String[] args) throws Exception { | |
final ExecutorService executor = Executors.newCachedThreadPool(); | |
((ThreadPoolExecutor) executor).setKeepAliveTime(3, TimeUnit.SECONDS); | |
final ConcurrentAuthenticator inst = new ConcurrentAuthenticatorUnsafe() { | |
@Override | |
protected PasswordAuthentication getPasswordAuthentication(final Thread callerThread) { | |
String path = null; | |
final URL u = getRequestingURL(); | |
path = u == null ? getRequestingPrompt() : u.getPath(); | |
System.out.println("requesting auth for " + path); | |
try { | |
Thread.sleep(2000); | |
} catch (final InterruptedException e) { | |
Thread.currentThread().interrupt(); | |
throw new RuntimeException(e); | |
} | |
System.out.println("got auth for " + path); | |
final String[] parts = path.split("/"); | |
final String user = parts[parts.length - 2]; | |
final String password = parts[parts.length - 1]; | |
return new PasswordAuthentication(user, password.toCharArray()); | |
} | |
}; | |
ConcurrentAuthenticator.setDefault(inst); | |
try { | |
final Future<Void> fut1 = executor.submit(mkTest("user1", "password1")); | |
final Callable<Void> test2 = mkTest("user2", "password2"); | |
// final Future<Void> fut2 = executor.submit(test2); | |
test2.call(); | |
fut1.get(); | |
// fut2.get(); | |
} finally { | |
executor.shutdown(); | |
} | |
// executor.awaitTermination(1, TimeUnit.MINUTES); | |
} | |
private static Callable<Void> mkTest(final String user, final String password) throws Exception { | |
return new Callable<Void>() { | |
@Override | |
public Void call() throws Exception { | |
final URL u = | |
new URL("http://httpbin.org/basic-auth/" + user + "/" + password + ""); | |
downloadContent(u); | |
// requestAuth(u); | |
return null; | |
} | |
}; | |
} | |
static void requestAuth(final URL u) { | |
if ("".length() == 0) { | |
ConcurrentAuthenticator.requestPasswordAuthentication(null, null, 0, null, u.getPath(), | |
null); | |
} else { | |
ConcurrentAuthenticator.requestPasswordAuthentication(null, null, 0, null, null, null, | |
u, null); | |
} | |
} | |
static void downloadContent(final URL u) throws Exception { | |
try (InputStream in = u.openStream()) { | |
int nb; | |
final byte[] buf = new byte[1024]; | |
while ((nb = in.read(buf)) != -1) { | |
System.out.write(buf, 0, nb); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.foo.concurrentauthenticator; | |
import java.lang.reflect.Field; | |
import java.net.PasswordAuthentication; | |
import sun.misc.Unsafe; | |
/** | |
* java.net.Authenticator disallows simultaneous password prompting from different threads, because | |
* the callback runs inside a synchronized block. This implementation circumvents it by calling | |
* {@link Unsafe#monitorExit(Object)} | |
*/ | |
@SuppressWarnings("restriction") | |
public abstract class ConcurrentAuthenticatorUnsafe extends ConcurrentAuthenticator { | |
private static final Unsafe UNSAFE; | |
static { | |
Unsafe uns = null; | |
try { | |
final Field singleoneInstanceField = Unsafe.class.getDeclaredField("theUnsafe"); | |
singleoneInstanceField.setAccessible(true); | |
uns = (Unsafe) singleoneInstanceField.get(null); | |
} catch (final Exception e) { | |
e.printStackTrace(); | |
} | |
UNSAFE = uns; | |
} | |
public ConcurrentAuthenticatorUnsafe() { | |
super(UNSAFE == null ? mkExecutor() : null); | |
} | |
@Override | |
protected PasswordAuthentication unlockAndGetPasswordAuthentication(final Thread callerThread, | |
final ConcurrentAuthenticator selfCopy) { | |
if (UNSAFE == null) { | |
return super.unlockAndGetPasswordAuthentication(callerThread, selfCopy); | |
} else { | |
final Class<?> authClazz = ConcurrentAuthenticator.class.getSuperclass(); | |
final int n = unlock(ConcurrentAuthenticatorUnsafe.this); | |
final int m = unlock(authClazz); | |
try { | |
return selfCopy.getPasswordAuthentication(callerThread); | |
} catch (final Exception e) { | |
throw new RuntimeException(e); | |
} finally { | |
relock(authClazz, m); | |
relock(ConcurrentAuthenticatorUnsafe.this, n); | |
} | |
} | |
} | |
private static void relock(final Object o, final int times) { | |
int n = times; | |
for (; n > 0; n--) { | |
UNSAFE.monitorEnter(o); | |
} | |
} | |
private static int unlock(final Object o) { | |
int n = 0; | |
try { | |
for (;; n++) { | |
UNSAFE.monitorExit(o); | |
} | |
} catch (final IllegalMonitorStateException e) { | |
} | |
return n; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment