/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.client.session;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.sshd.client.ClientAuthenticationManager;
import org.apache.sshd.client.auth.UserAuth;
import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.future.DefaultAuthFuture;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.RuntimeSshException;
import org.apache.sshd.common.Service;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.session.SessionHolder;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.closeable.AbstractCloseable;
/**
* Client side <code>ssh-auth</code> service.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class ClientUserAuthService
extends AbstractCloseable
implements Service, SessionHolder<ClientSession>, ClientSessionHolder {
/**
* The AuthFuture that is being used by the current auth request. This encodes the state.
* isSuccess -> authenticated, else if isDone -> server waiting for user auth, else authenticating.
*/
private final AtomicReference<AuthFuture> authFutureHolder = new AtomicReference<>();
private final ClientSessionImpl clientSession;
private final List<String> clientMethods;
private final List<NamedFactory<UserAuth>> authFactories;
private String service;
private List<String> serverMethods;
private UserAuth userAuth;
private int currentMethod;
public ClientUserAuthService(Session s) {
clientSession = ValidateUtils.checkInstanceOf(s, ClientSessionImpl.class, "Client side service used on server side: %s", s);
authFactories = ValidateUtils.checkNotNullAndNotEmpty(
clientSession.getUserAuthFactories(), "No user auth factories for %s", s);
clientMethods = new ArrayList<>();
String prefs = s.getString(ClientAuthenticationManager.PREFERRED_AUTHS);
if (GenericUtils.isEmpty(prefs)) {
for (NamedFactory<UserAuth> factory : authFactories) {
clientMethods.add(factory.getName());
}
} else {
if (log.isDebugEnabled()) {
log.debug("ClientUserAuthService({}) use configured preferences: {}", s, prefs);
}
for (String pref : GenericUtils.split(prefs, ',')) {
NamedFactory<UserAuth> factory = NamedResource.findByName(pref, String.CASE_INSENSITIVE_ORDER, authFactories);
if (factory != null) {
clientMethods.add(pref);
} else {
if (log.isDebugEnabled()) {
log.debug("ClientUserAuthService({}) skip unknown preferred authentication method: {}", s, pref);
}
}
}
}
if (log.isDebugEnabled()) {
log.debug("ClientUserAuthService({}) client methods: {}", s, clientMethods);
}
}
@Override
public ClientSession getSession() {
return getClientSession();
}
@Override
public ClientSession getClientSession() {
return clientSession;
}
@Override
public void start() {
// ignored
}
public AuthFuture auth(String service) throws IOException {
this.service = ValidateUtils.checkNotNullAndNotEmpty(service, "No service name");
ClientSession session = getClientSession();
// check if any previous future in use
AuthFuture authFuture = new DefaultAuthFuture(clientSession.getLock());
AuthFuture currentFuture = authFutureHolder.getAndSet(authFuture);
if (currentFuture != null) {
if (currentFuture.isDone()) {
if (log.isDebugEnabled()) {
log.debug("auth({})[{}] request new authentication", session, service);
}
} else {
currentFuture.setException(new InterruptedIOException("New authentication started before previous completed"));
}
}
// start from scratch
serverMethods = null;
currentMethod = 0;
if (userAuth != null) {
try {
userAuth.destroy();
} finally {
userAuth = null;
}
}
if (log.isDebugEnabled()) {
log.debug("auth({})[{}] send SSH_MSG_USERAUTH_REQUEST for 'none'", session, service);
}
String username = session.getUsername();
Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST, username.length() + service.length() + Integer.SIZE);
buffer.putString(username);
buffer.putString(service);
buffer.putString("none");
session.writePacket(buffer);
return authFuture;
}
@Override
public void process(int cmd, Buffer buffer) throws Exception {
ClientSession session = getClientSession();
AuthFuture authFuture = authFutureHolder.get();
if ((authFuture != null) && authFuture.isSuccess()) {
log.error("process({}) unexpected authenticated client command: {}",
session, SshConstants.getCommandMessageName(cmd));
throw new IllegalStateException("UserAuth message delivered to authenticated client");
} else if ((authFuture != null) && authFuture.isDone()) {
// ignore for now; TODO: random packets
if (log.isDebugEnabled()) {
log.debug("process({}) Ignoring random message - cmd={}",
session, SshConstants.getCommandMessageName(cmd));
}
} else if (cmd == SshConstants.SSH_MSG_USERAUTH_BANNER) {
String welcome = buffer.getString();
String lang = buffer.getString();
if (log.isDebugEnabled()) {
log.debug("process({}) Welcome banner(lang={}): {}", session, lang, welcome);
}
UserInteraction ui = session.getUserInteraction();
try {
if ((ui != null) && ui.isInteractionAllowed(session)) {
ui.welcome(session, welcome, lang);
}
} catch (Error e) {
log.warn("process({}) failed ({}) to consult interaction: {}",
session, e.getClass().getSimpleName(), e.getMessage());
if (log.isDebugEnabled()) {
log.debug("process(" + session + ") interaction consultation failure details", e);
}
throw new RuntimeSshException(e);
}
} else {
buffer.rpos(buffer.rpos() - 1);
processUserAuth(buffer);
}
}
/**
* Execute one step in user authentication.
*
* @param buffer The input {@link Buffer}
* @throws Exception If failed to process
*/
protected void processUserAuth(Buffer buffer) throws Exception {
int cmd = buffer.getUByte();
ClientSession session = getClientSession();
if (cmd == SshConstants.SSH_MSG_USERAUTH_SUCCESS) {
if (log.isDebugEnabled()) {
log.debug("processUserAuth({}) SSH_MSG_USERAUTH_SUCCESS Succeeded with {}",
session, (userAuth == null) ? "<unknown>" : userAuth.getName());
}
if (userAuth != null) {
try {
userAuth.destroy();
} finally {
userAuth = null;
}
}
session.setAuthenticated();
((ClientSessionImpl) session).switchToNextService();
AuthFuture authFuture = Objects.requireNonNull(authFutureHolder.get(), "No current future");
// Will wake up anyone sitting in waitFor
authFuture.setAuthed(true);
return;
}
if (cmd == SshConstants.SSH_MSG_USERAUTH_FAILURE) {
String mths = buffer.getString();
boolean partial = buffer.getBoolean();
if (log.isDebugEnabled()) {
log.debug("processUserAuth({}) Received SSH_MSG_USERAUTH_FAILURE - partial={}, methods={}",
session, partial, mths);
}
if (partial || (serverMethods == null)) {
serverMethods = Arrays.asList(GenericUtils.split(mths, ','));
currentMethod = 0;
if (userAuth != null) {
try {
userAuth.destroy();
} finally {
userAuth = null;
}
}
}
tryNext(cmd);
return;
}
if (userAuth == null) {
throw new IllegalStateException("Received unknown packet: " + SshConstants.getCommandMessageName(cmd));
}
if (log.isDebugEnabled()) {
log.debug("processUserAuth({}) delegate processing of {} to {}",
session, SshConstants.getCommandMessageName(cmd), userAuth.getName());
}
buffer.rpos(buffer.rpos() - 1);
if (!userAuth.process(buffer)) {
tryNext(cmd);
}
}
protected void tryNext(int cmd) throws Exception {
ClientSession session = getClientSession();
// Loop until we find something to try
while (true) {
if (userAuth == null) {
if (log.isDebugEnabled()) {
log.debug("tryNext({}) starting authentication mechanisms: client={}, server={}",
session, clientMethods, serverMethods);
}
} else if (!userAuth.process(null)) {
if (log.isDebugEnabled()) {
log.debug("tryNext({}) no initial request sent by method={}", session, userAuth.getName());
}
try {
userAuth.destroy();
} finally {
userAuth = null;
}
currentMethod++;
} else {
if (log.isDebugEnabled()) {
log.debug("tryNext({}) successfully processed initial buffer by method={}", session, userAuth.getName());
}
return;
}
String method = null;
for (; currentMethod < clientMethods.size(); currentMethod++) {
method = clientMethods.get(currentMethod);
if (serverMethods.contains(method)) {
break;
}
}
if (currentMethod >= clientMethods.size()) {
if (log.isDebugEnabled()) {
log.debug("tryNext({}) exhausted all methods - client={}, server={}",
session, clientMethods, serverMethods);
}
// also wake up anyone sitting in waitFor
AuthFuture authFuture = Objects.requireNonNull(authFutureHolder.get(), "No current future");
authFuture.setException(new SshException(SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, "No more authentication methods available"));
return;
}
userAuth = NamedFactory.create(authFactories, method);
if (userAuth == null) {
throw new UnsupportedOperationException("Failed to find a user-auth factory for method=" + method);
}
if (log.isDebugEnabled()) {
log.debug("tryNext({}) attempting method={}", session, method);
}
userAuth.init(session, service);
}
}
@Override
protected void preClose() {
AuthFuture authFuture = authFutureHolder.get();
if ((authFuture != null) && (!authFuture.isDone())) {
authFuture.setException(new SshException("Session is closed"));
}
super.preClose();
}
}