package io.vertx.ext.web.handler.impl;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.htdigest.HtdigestAuth;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import io.vertx.ext.web.handler.DigestAuthHandler;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DigestAuthHandlerImpl extends AuthorizationAuthHandler implements DigestAuthHandler {
private static class Nonce {
private final long createdAt;
private int count;
Nonce() {
createdAt = System.currentTimeMillis();
count = 0;
}
}
private static final Pattern PARSER = Pattern.compile("(\\w+)=[\"]?([^\"]*)[\"]?$");
private static final Pattern SPLITTER = Pattern.compile(",(?=(?:[^\"]|\"[^\"]*\")*$)");
private static final MessageDigest MD5;
static {
try {
MD5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private final SecureRandom random = new SecureRandom();
private final Map<String, Nonce> nonces = new HashMap<>();
private final long nonceExpireTimeout;
private long lastExpireRun;
public DigestAuthHandlerImpl(HtdigestAuth authProvider, long nonceExpireTimeout) {
super(authProvider, authProvider.realm(), Type.DIGEST);
this.nonceExpireTimeout = nonceExpireTimeout;
}
@Override
public void parseCredentials(RoutingContext context, Handler<AsyncResult<JsonObject>> handler) {
long now = System.currentTimeMillis();
if (now - lastExpireRun > nonceExpireTimeout / 2) {
nonces.entrySet().removeIf(entry -> entry.getValue().createdAt + nonceExpireTimeout < now);
lastExpireRun = now;
}
parseAuthorization(context, false, parseAuthorization -> {
if (parseAuthorization.failed()) {
handler.handle(Future.failedFuture(parseAuthorization.cause()));
return;
}
final JsonObject authInfo = new JsonObject();
try {
String[] tokens = SPLITTER.split(parseAuthorization.result());
int i = 0;
int len = tokens.length;
while (i < len) {
Matcher m = PARSER.matcher(tokens[i]);
if (m.find()) {
authInfo.put(m.group(1), m.group(2));
}
++i;
}
final String nonce = authInfo.getString("nonce");
if (!nonces.containsKey(nonce)) {
handler.handle(Future.failedFuture(UNAUTHORIZED));
return;
}
if (authInfo.containsKey("qop")) {
int nc = Integer.parseInt(authInfo.getString("nc"));
final Nonce n = nonces.get(nonce);
if (nc <= n.count) {
handler.handle(Future.failedFuture(UNAUTHORIZED));
return;
}
n.count = nc;
}
} catch (RuntimeException e) {
handler.handle(Future.failedFuture(e));
}
final Session session = context.session();
if (session != null) {
String opaque = (String) session.data().get("opaque");
if (opaque != null && !opaque.equals(authInfo.getString("opaque"))) {
handler.handle(Future.failedFuture(UNAUTHORIZED));
return;
}
}
authInfo.put("method", context.request().method().name());
handler.handle(Future.succeededFuture(authInfo));
});
}
@Override
protected String authenticateHeader(RoutingContext context) {
final byte[] bytes = new byte[32];
random.nextBytes(bytes);
String nonce = md5(bytes);
nonces.put(nonce, new Nonce());
String opaque = null;
final Session session = context.session();
if (session != null) {
opaque = (String) session.data().get("opaque");
}
if (opaque == null) {
random.nextBytes(bytes);
opaque = md5(bytes);
}
return "Digest realm=\"" + realm + "\", qop=\"auth\", nonce=\"" + nonce + "\", opaque=\"" + opaque + "\"";
}
private final static char[] hexArray = "0123456789abcdef".toCharArray();
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
private static synchronized String md5(byte[] payload) {
MD5.reset();
return bytesToHex(MD5.digest(payload));
}
}