blob: 7ad8d837b93bf982701a963a784d054022cad46a [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.rj.server.srvext;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import java.rmi.server.RemoteServer;
import java.rmi.server.ServerNotActiveException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.util.Locale;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.ChoiceCallback;
import javax.security.auth.callback.ConfirmationCallback;
import javax.security.auth.callback.LanguageCallback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.TextInputCallback;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.rj.RjException;
import org.eclipse.statet.rj.RjInitFailedException;
import org.eclipse.statet.rj.server.ServerLogin;
/**
* Abstract class for authentication methods.
*
* An authentication method must extend this class and implement the
* abstract methods:
* <li>{@link #doInit(String)}</li>
* <li>{@link #doCreateLogin()}</li>
* <li>{@link #doPerformLogin(Callback[])}</li>
*/
@NonNullByDefault
public abstract class ServerAuthMethod {
protected static void copyAnswer(final ImList<Callback> from, final Callback[] to) throws UnsupportedCallbackException {
if (from.size() != to.length) {
throw new IllegalArgumentException();
}
for (int i= 0; i < from.size(); i++) {
final Callback source= from.get(i);
if (source instanceof TextOutputCallback) {
continue;
}
if (source instanceof NameCallback) {
((NameCallback)to[i]).setName(((NameCallback)source).getName());
continue;
}
if (source instanceof PasswordCallback) {
((PasswordCallback)to[i]).setPassword(((PasswordCallback)source).getPassword());
((PasswordCallback)source).clearPassword();
continue;
}
if (source instanceof TextInputCallback) {
((TextInputCallback)to[i]).setText(((TextInputCallback)source).getText());
continue;
}
if (source instanceof ChoiceCallback) {
final int[] selectedIndexes= ((ChoiceCallback)source).getSelectedIndexes();
if (((ChoiceCallback)source).allowMultipleSelections()) {
((ChoiceCallback)to[i]).setSelectedIndexes(selectedIndexes);
}
else if (selectedIndexes.length == 1) {
((ChoiceCallback)to[i]).setSelectedIndex(selectedIndexes[0]);
}
continue;
}
if (source instanceof ConfirmationCallback) {
((ConfirmationCallback)to[i]).setSelectedIndex(((ConfirmationCallback)source).getSelectedIndex());
continue;
}
if (source instanceof LanguageCallback) {
((LanguageCallback)to[i]).setLocale(Locale.getDefault());
continue;
}
throw new UnsupportedCallbackException(to[i]);
}
}
private static final Logger LOGGER= Logger.getLogger("org.eclipse.statet.rj.server.auth");
private final String logPrefix;
private final String id;
private final Random randomGenerator= new SecureRandom();
private final boolean usePubkeyExchange;
private @Nullable KeyPairGenerator keyPairGenerator;
private @Nullable String pendingLoginClient;
private long pendingLoginId;
private @Nullable KeyPair pendingLoginKeyPair;
private @Nullable String expliciteClient;
private boolean isInitialized;
/**
*
* @param usePubkeyExchange enables default encryption of secret data (password)
*/
protected ServerAuthMethod(final String id, final boolean usePubkeyExchange) {
this.usePubkeyExchange= usePubkeyExchange;
this.id= id;
this.logPrefix= "[Auth:" + id + "]";
}
public void setExpliciteClient(final String client) {
this.expliciteClient= client;
}
private String getCallingClient() throws ServerNotActiveException {
final String expliciteClient= this.expliciteClient;
if (expliciteClient != null) {
return expliciteClient;
}
return RemoteServer.getClientHost();
}
public boolean isValid(final Client client) {
try {
return (getCallingClient().equals(client.clientId));
}
catch (final ServerNotActiveException e) {
return false;
}
}
public final void init(final @Nullable String arg) throws RjException {
try {
if (this.usePubkeyExchange) {
final KeyPairGenerator keyPairGenerator= KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
this.keyPairGenerator= keyPairGenerator;
}
doInit(arg);
this.isInitialized= true;
}
catch (final Exception e) {
final RjException rje= (e instanceof RjException) ? (RjException) e :
new RjInitFailedException("An error occurred when initializing authentication method '"+this.id+"'.", e);
throw rje;
}
finally {
System.gc();
}
}
protected final Random getRandom() {
return this.randomGenerator;
}
protected final void assertInitialized() {
if (!this.isInitialized) {
throw new IllegalStateException("not initialized");
}
}
/**
* Is called when the server is started
*
* @param arg configuration argument
*
* @throws RjException
*/
protected abstract void doInit(@Nullable String arg) throws RjException;
public final ServerLogin createLogin() throws RjException {
assertInitialized();
try {
final String client= getCallingClient();
final boolean same= client.equals(this.pendingLoginClient);
this.pendingLoginClient= client;
LOGGER.log(Level.INFO, "{0} creating new login ({1}).",
new Object[] { this.logPrefix, client });
long nextId;
do {
nextId= this.randomGenerator.nextLong();
} while (nextId == this.pendingLoginId);
this.pendingLoginId= nextId;
if (this.usePubkeyExchange) {
if (this.pendingLoginKeyPair == null || !same) {
final KeyPairGenerator keyPairGenerator= nonNullAssert(this.keyPairGenerator);
this.pendingLoginKeyPair= nonNullAssert(keyPairGenerator).generateKeyPair();
}
}
return createNewLogin(doCreateLogin());
}
catch (final Exception e) {
final RjException rje= (e instanceof RjException) ? (RjException) e :
new RjException("An unexpected error occurred when preparing login process.", e);
throw rje;
}
}
/**
* Is called when client initiates a login process.
*
* @return login callbacks to handle by the client
*
* @throws RjException
*/
protected abstract ImList<Callback> doCreateLogin() throws RjException;
private ServerLogin createNewLogin(final ImList<Callback> callbacks) {
if (this.usePubkeyExchange) {
final KeyPair loginKeyPair= nonNullAssert(this.pendingLoginKeyPair);
return new ServerLogin(this.pendingLoginId, loginKeyPair.getPublic(), callbacks);
}
else {
return new ServerLogin(this.pendingLoginId, null, callbacks);
}
}
public final Client performLogin(final ServerLogin login) throws RjException, LoginException {
assertInitialized();
String client= null;
try {
client= getCallingClient();
if (login.getId() != this.pendingLoginId ||
!client.equals(this.pendingLoginClient)) {
throw new FailedLoginException("Login process was interrupted by another client.");
}
if (this.usePubkeyExchange) {
final KeyPair loginKeyPair= nonNullAssert(this.pendingLoginKeyPair);
login.readAnswer(loginKeyPair.getPrivate());
}
else {
login.readAnswer(null);
}
this.pendingLoginKeyPair= null;
final String name= doPerformLogin(login.getCallbacks());
LOGGER.log(Level.INFO, "{0} performing login completed successfull: {1} ({2}).",
new Object[] { this.logPrefix, name, client });
return new Client(name, getCallingClient(), (byte)0);
}
catch (final LoginException e) {
final LogRecord log= new LogRecord(Level.INFO, "{0} performing login failed ({1}).");
log.setParameters(new @Nullable Object[] { this.logPrefix, client });
log.setThrown(e);
LOGGER.log(log);
throw e;
}
catch (final RjException e) {
throw e;
}
catch (final Exception e) {
throw new RjException("An unexpected error occurred when validating the login credential.", e);
}
finally {
System.gc();
}
}
/**
* This method is called when the client sends the login data
*
* @param callbacks the callbacks handled by the client (note, the callbacks are not
* the same instances returned by {@link #createLogin()}, but clones)
*
* @return login id like username
*
* @throws LoginException if login failed (but can usually fixed by other login data)
* @throws RjException
*/
protected abstract String doPerformLogin(ImList<Callback> callbacks) throws LoginException, RjException;
}