/**
* This file is part of git-as-svn. It is subject to the license terms
* in the LICENSE file found in the top-level directory of this distribution
* and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
* including this file, may be copied, modified, propagated, or distributed
* except according to the terms contained in the LICENSE file.
*/
package svnserver.auth.cache;
import com.google.common.cache.Cache;
import org.apache.commons.codec.binary.Hex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.tmatesoft.svn.core.SVNException;
import svnserver.HashHelper;
import svnserver.auth.Authenticator;
import svnserver.auth.PlainAuthenticator;
import svnserver.auth.User;
import svnserver.auth.UserDB;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
/**
* Caching user authentication result for reduce external API usage.
*
* @author Artem V. Navrotskiy
*/
public class CacheUserDB implements UserDB {
@NotNull
private final Collection<Authenticator> authenticators = Collections.singleton(new PlainAuthenticator(this));
@NotNull
private final static User invalidUser = User.create("invalid", "invalid", null, null);
@NotNull
private final UserDB userDB;
@NotNull
private final Cache<String, User> cache;
public CacheUserDB(@NotNull UserDB userDB, @NotNull Cache<String, User> cache) {
this.userDB = userDB;
this.cache = cache;
}
@Nullable
@Override
public User check(@NotNull String userName, @NotNull String password) throws SVNException, IOException {
return cached("c." + hash(userName, password), db -> db.check(userName, password));
}
@Nullable
@Override
public User lookupByUserName(@NotNull String userName) throws SVNException, IOException {
return cached("l." + userName, db -> db.lookupByUserName(userName));
}
@Nullable
@Override
public User lookupByExternal(@NotNull String external) throws SVNException, IOException {
return cached("e." + external, db -> db.lookupByExternal(external));
}
private User cached(@NotNull String key, @NotNull CachedCallback callback) throws IOException, SVNException {
try {
final User cachedUser = cache.get(key, () -> {
final User authUser = callback.exec(userDB);
return authUser != null ? authUser : invalidUser;
});
return cachedUser != invalidUser ? cachedUser : null;
} catch (ExecutionException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
if (e.getCause() instanceof SVNException) {
throw (SVNException) e.getCause();
}
throw new IllegalStateException(e);
}
}
@NotNull
private String hash(@NotNull String userName, @NotNull String password) {
final MessageDigest digest = HashHelper.sha256();
hashPacket(digest, userName.getBytes(StandardCharsets.UTF_8));
hashPacket(digest, password.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(digest.digest());
}
private void hashPacket(@NotNull MessageDigest digest, @NotNull byte[] packet) {
int length = packet.length;
for (; ; ) {
digest.update((byte) (length & 0xFF));
if (length == 0) {
break;
}
length = (length >> 8) & 0xFFFFFF;
}
digest.update(packet);
}
@FunctionalInterface
private interface CachedCallback {
@Nullable
User exec(UserDB userDB) throws SVNException, IOException;
}
@NotNull
@Override
public Collection<Authenticator> authenticators() {
return authenticators;
}
}