blob: 3b3c8c619f0a84af60f0b273bb41ead93bb87f7a [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2019 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 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.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>
*/
public abstract class ServerAuthMethod {
protected static void copyAnswer(final Callback[] from, final Callback[] to) throws UnsupportedCallbackException {
assert (from.length == to.length);
for (int i= 0; i < from.length; i++) {
if (from[i] instanceof TextOutputCallback) {
continue;
}
if (from[i] instanceof NameCallback) {
((NameCallback) to[i]).setName(((NameCallback) from[i]).getName());
continue;
}
if (from[i] instanceof PasswordCallback) {
((PasswordCallback) to[i]).setPassword(((PasswordCallback) from[i]).getPassword());
((PasswordCallback) from[i]).clearPassword();
continue;
}
if (from[i] instanceof TextInputCallback) {
((TextInputCallback) to[i]).setText(((TextInputCallback) from[i]).getText());
continue;
}
if (from[i] instanceof ChoiceCallback) {
final int[] selectedIndexes= ((ChoiceCallback) from[i]).getSelectedIndexes();
if (((ChoiceCallback) from[i]).allowMultipleSelections()) {
((ChoiceCallback) to[i]).setSelectedIndexes(selectedIndexes);
}
else if (selectedIndexes.length == 1) {
((ChoiceCallback) to[i]).setSelectedIndex(selectedIndexes[0]);
}
continue;
}
if (from[i] instanceof ConfirmationCallback) {
((ConfirmationCallback) to[i]).setSelectedIndex(((ConfirmationCallback) from[i]).getSelectedIndex());
continue;
}
if (from[i] 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 Random randomGenerator;
private final boolean usePubkeyExchange;
private KeyPairGenerator keyPairGenerator;
private String pendingLoginClient;
private long pendingLoginId;
private KeyPair pendingLoginKeyPair;
private String expliciteClient;
/**
*
* @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 {
if (this.expliciteClient != null) {
return this.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 String arg) throws RjException {
try {
if (this.usePubkeyExchange) {
this.keyPairGenerator= KeyPairGenerator.getInstance("RSA");
this.keyPairGenerator.initialize(2048);
}
this.randomGenerator= new SecureRandom();
doInit(arg);
}
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;
}
}
protected final Random getRandom() {
return this.randomGenerator;
}
/**
* Is called when the server is started
*
* @param arg configuration argument
*
* @throws RjException
*/
protected abstract void doInit(String arg) throws RjException;
public final ServerLogin createLogin() throws RjException {
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) {
this.pendingLoginKeyPair= this.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 Callback[] doCreateLogin() throws RjException;
private ServerLogin createNewLogin(final Callback[] callbacks) {
if (this.usePubkeyExchange) {
return new ServerLogin(this.pendingLoginId, this.pendingLoginKeyPair.getPublic(), callbacks);
}
else {
return new ServerLogin(this.pendingLoginId, null, callbacks);
}
}
public final Client performLogin(final ServerLogin login) throws RjException, LoginException {
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.");
}
login.readAnswer(this.usePubkeyExchange ? this.pendingLoginKeyPair.getPrivate() : 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 Exception e) {
if (e instanceof LoginException) {
final LogRecord log= new LogRecord(Level.INFO, "{0} performing login failed ({1}).");
log.setParameters(new Object[] { this.logPrefix, client });
log.setThrown(e);
LOGGER.log(log);
throw (LoginException) e;
}
if (e instanceof RjException) {
throw (RjException) 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(Callback[] callbacks) throws LoginException, RjException;
}