blob: 345c0b39dfe864764ef34eb4022f94b0ba8629ce [file] [log] [blame]
/*
* 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.server;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.sshd.common.Closeable;
import org.apache.sshd.common.Factory;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.ServiceFactory;
import org.apache.sshd.common.config.SshConfigFileReader;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.helpers.AbstractFactoryManager;
import org.apache.sshd.common.io.IoAcceptor;
import org.apache.sshd.common.io.IoServiceFactory;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.mina.MinaServiceFactory;
import org.apache.sshd.common.io.nio2.Nio2ServiceFactory;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.server.auth.UserAuth;
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator;
import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
import org.apache.sshd.server.forward.ForwardingFilter;
import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.scp.ScpCommandFactory;
import org.apache.sshd.server.session.ServerConnectionServiceFactory;
import org.apache.sshd.server.session.ServerProxyAcceptor;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.session.ServerUserAuthServiceFactory;
import org.apache.sshd.server.session.SessionFactory;
import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
import org.apache.sshd.server.shell.ProcessShellFactory;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
/**
* <p>
* The SshServer class is the main entry point for the server side of the SSH protocol.
* </p>
*
* <p>
* The SshServer has to be configured before being started. Such configuration can be
* done either using a dependency injection mechanism (such as the Spring framework)
* or programmatically. Basic setup is usually done using the {@link #setUpDefaultServer()}
* method, which will known ciphers, macs, channels, etc...
* Besides this basic setup, a few things have to be manually configured such as the
* port number, {@link Factory}, the {@link org.apache.sshd.common.keyprovider.KeyPairProvider}
* and the {@link PasswordAuthenticator}.
* </p>
*
* <p>
* Some properties can also be configured using the {@link PropertyResolverUtils}
* {@code updateProperty} methods.
* </p>
*
* Once the SshServer instance has been configured, it can be started using the
* {@link #start()} method and stopped using the {@link #stop()} method.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
* @see ServerFactoryManager
* @see org.apache.sshd.common.FactoryManager
*/
public class SshServer extends AbstractFactoryManager implements ServerFactoryManager, Closeable {
public static final Factory<SshServer> DEFAULT_SSH_SERVER_FACTORY = new Factory<SshServer>() {
@Override
public SshServer create() {
return new SshServer();
}
};
public static final List<ServiceFactory> DEFAULT_SERVICE_FACTORIES =
Collections.unmodifiableList(Arrays.asList(
ServerUserAuthServiceFactory.INSTANCE,
ServerConnectionServiceFactory.INSTANCE
));
protected IoAcceptor acceptor;
protected String host;
protected int port;
private ServerProxyAcceptor proxyAcceptor;
private Factory<Command> shellFactory;
private SessionFactory sessionFactory;
private CommandFactory commandFactory;
private List<NamedFactory<Command>> subsystemFactories;
private List<NamedFactory<UserAuth>> userAuthFactories;
private PasswordAuthenticator passwordAuthenticator;
private PublickeyAuthenticator publickeyAuthenticator;
private KeyboardInteractiveAuthenticator interactiveAuthenticator;
private HostBasedAuthenticator hostBasedAuthenticator;
private GSSAuthenticator gssAuthenticator;
public SshServer() {
super();
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
/**
* Configure the port number to use for this SSH server.
*
* @param port the port number for this SSH server
*/
public void setPort(int port) {
this.port = port;
}
@Override
public List<NamedFactory<UserAuth>> getUserAuthFactories() {
return userAuthFactories;
}
@Override
public void setUserAuthFactories(List<NamedFactory<UserAuth>> userAuthFactories) {
this.userAuthFactories = userAuthFactories;
}
@Override
public Factory<Command> getShellFactory() {
return shellFactory;
}
public void setShellFactory(Factory<Command> shellFactory) {
this.shellFactory = shellFactory;
}
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public ServerProxyAcceptor getServerProxyAcceptor() {
return proxyAcceptor;
}
@Override
public void setServerProxyAcceptor(ServerProxyAcceptor proxyAcceptor) {
this.proxyAcceptor = proxyAcceptor;
}
@Override
public CommandFactory getCommandFactory() {
return commandFactory;
}
public void setCommandFactory(CommandFactory commandFactory) {
this.commandFactory = commandFactory;
}
@Override
public List<NamedFactory<Command>> getSubsystemFactories() {
return subsystemFactories;
}
public void setSubsystemFactories(List<NamedFactory<Command>> subsystemFactories) {
this.subsystemFactories = subsystemFactories;
}
@Override
public PasswordAuthenticator getPasswordAuthenticator() {
return passwordAuthenticator;
}
@Override
public void setPasswordAuthenticator(PasswordAuthenticator passwordAuthenticator) {
this.passwordAuthenticator = passwordAuthenticator;
}
@Override
public PublickeyAuthenticator getPublickeyAuthenticator() {
return publickeyAuthenticator;
}
@Override
public void setPublickeyAuthenticator(PublickeyAuthenticator publickeyAuthenticator) {
this.publickeyAuthenticator = publickeyAuthenticator;
}
@Override
public KeyboardInteractiveAuthenticator getKeyboardInteractiveAuthenticator() {
return interactiveAuthenticator;
}
@Override
public void setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator interactiveAuthenticator) {
this.interactiveAuthenticator = interactiveAuthenticator;
}
@Override
public GSSAuthenticator getGSSAuthenticator() {
return gssAuthenticator;
}
@Override
public void setGSSAuthenticator(GSSAuthenticator gssAuthenticator) {
this.gssAuthenticator = gssAuthenticator;
}
@Override
public HostBasedAuthenticator getHostBasedAuthenticator() {
return hostBasedAuthenticator;
}
@Override
public void setHostBasedAuthenticator(HostBasedAuthenticator hostBasedAuthenticator) {
this.hostBasedAuthenticator = hostBasedAuthenticator;
}
@Override
public void setTcpipForwardingFilter(ForwardingFilter forwardingFilter) {
this.tcpipForwardingFilter = forwardingFilter;
}
@Override
protected void checkConfig() {
super.checkConfig();
ValidateUtils.checkTrue(getPort() >= 0 /* zero means not set yet */, "Bad port number: %d", Integer.valueOf(getPort()));
List<NamedFactory<UserAuth>> authFactories = ServerAuthenticationManager.Utils.resolveUserAuthFactories(this);
setUserAuthFactories(ValidateUtils.checkNotNullAndNotEmpty(authFactories, "UserAuthFactories not set"));
ValidateUtils.checkNotNullAndNotEmpty(getChannelFactories(), "ChannelFactories not set");
ValidateUtils.checkNotNull(getKeyPairProvider(), "HostKeyProvider not set");
ValidateUtils.checkNotNull(getFileSystemFactory(), "FileSystemFactory not set");
if (GenericUtils.isEmpty(getServiceFactories())) {
setServiceFactories(DEFAULT_SERVICE_FACTORIES);
}
}
/**
* Start the SSH server and accept incoming exceptions on the configured port.
*
* @throws IOException If failed to start
*/
public void start() throws IOException {
checkConfig();
if (sessionFactory == null) {
sessionFactory = createSessionFactory();
}
acceptor = createAcceptor();
setupSessionTimeout(sessionFactory);
String hostsList = getHost();
if (!GenericUtils.isEmpty(hostsList)) {
String[] hosts = GenericUtils.split(hostsList, ',');
for (String host : hosts) {
if (log.isDebugEnabled()) {
log.debug("start() - resolve bind host={}", host);
}
InetAddress[] inetAddresses = InetAddress.getAllByName(host);
for (InetAddress inetAddress : inetAddresses) {
if (log.isTraceEnabled()) {
log.trace("start() - bind host={} / {}", host, inetAddress);
}
acceptor.bind(new InetSocketAddress(inetAddress, port));
if (port == 0) {
port = ((InetSocketAddress) acceptor.getBoundAddresses().iterator().next()).getPort();
log.info("start() listen on auto-allocated port=" + port);
}
}
}
} else {
acceptor.bind(new InetSocketAddress(port));
if (port == 0) {
port = ((InetSocketAddress) acceptor.getBoundAddresses().iterator().next()).getPort();
log.info("start() listen on auto-allocated port=" + port);
}
}
}
/**
* Stop the SSH server. This method will block until all resources are actually disposed.
* @throws IOException if stopping failed somehow
*/
public void stop() throws IOException {
stop(false);
}
public void stop(boolean immediately) throws IOException {
long maxWait = immediately ? PropertyResolverUtils.getLongProperty(this, STOP_WAIT_TIME, DEFAULT_STOP_WAIT_TIME) : Long.MAX_VALUE;
boolean successful = close(immediately).await(maxWait);
if (!successful) {
throw new SocketTimeoutException("Failed to receive closure confirmation within " + maxWait + " millis");
}
}
public void open() throws IOException {
start();
}
@Override
protected Closeable getInnerCloseable() {
return builder()
.run(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
removeSessionTimeout(sessionFactory);
}
})
.sequential(acceptor, ioServiceFactory)
.run(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
acceptor = null;
ioServiceFactory = null;
if (shutdownExecutor && (executor != null) && (!executor.isShutdown())) {
try {
executor.shutdownNow();
} finally {
executor = null;
}
}
}
})
.build();
}
/**
* Obtain the list of active sessions.
*
* @return A {@link List} of the currently active session
*/
public List<AbstractSession> getActiveSessions() {
List<AbstractSession> sessions = new ArrayList<>();
for (IoSession ioSession : acceptor.getManagedSessions().values()) {
AbstractSession session = AbstractSession.getSession(ioSession, true);
if (session != null) {
sessions.add(session);
}
}
return sessions;
}
protected IoAcceptor createAcceptor() {
return getIoServiceFactory().createAcceptor(getSessionFactory());
}
protected SessionFactory createSessionFactory() {
return new SessionFactory(this);
}
@Override
public String toString() {
return "SshServer[" + Integer.toHexString(hashCode()) + "]";
}
public static SshServer setUpDefaultServer() {
return ServerBuilder.builder().build();
}
/*=================================
Main class implementation
*=================================*/
public static void main(String[] args) throws Exception {
int port = 8000;
String provider;
boolean error = false;
String hostKeyType = AbstractGeneratorHostKeyProvider.DEFAULT_ALGORITHM;
int hostKeySize = 0;
Map<String, String> options = new LinkedHashMap<>();
int numArgs = GenericUtils.length(args);
for (int i = 0; i < numArgs; i++) {
String argName = args[i];
if ("-p".equals(argName)) {
if (i + 1 >= numArgs) {
System.err.println("option requires an argument: " + argName);
break;
}
port = Integer.parseInt(args[++i]);
} else if ("-key-type".equals(argName)) {
if (i + 1 >= numArgs) {
System.err.println("option requires an argument: " + argName);
break;
}
hostKeyType = args[++i].toUpperCase();
} else if ("-key-size".equals(argName)) {
if (i + 1 >= numArgs) {
System.err.println("option requires an argument: " + argName);
break;
}
hostKeySize = Integer.parseInt(args[++i]);
} else if ("-io".equals(argName)) {
if (i + 1 >= numArgs) {
System.err.println("option requires an argument: " + argName);
break;
}
provider = args[++i];
if ("mina".equals(provider)) {
System.setProperty(IoServiceFactory.class.getName(), MinaServiceFactory.class.getName());
} else if ("nio2".endsWith(provider)) {
System.setProperty(IoServiceFactory.class.getName(), Nio2ServiceFactory.class.getName());
} else {
System.err.println("provider should be mina or nio2: " + argName);
break;
}
} else if ("-o".equals(argName)) {
if (i + 1 >= numArgs) {
System.err.println("option requires and argument: " + argName);
error = true;
break;
}
String opt = args[++i];
int idx = opt.indexOf('=');
if (idx <= 0) {
System.err.println("bad syntax for option: " + opt);
error = true;
break;
}
options.put(opt.substring(0, idx), opt.substring(idx + 1));
} else if (argName.startsWith("-")) {
System.err.println("illegal option: " + argName);
error = true;
break;
} else {
System.err.println("extra argument: " + argName);
error = true;
break;
}
}
if (error) {
System.err.println("usage: sshd [-p port] [-io mina|nio2] [-key-type RSA|DSA|EC] [-o option=value]");
System.exit(-1);
}
System.err.println("Starting SSHD on port " + port);
SshServer sshd = SshServer.setUpDefaultServer();
Map<String, Object> props = sshd.getProperties();
props.putAll(options);
setupServerBanner(sshd, options);
sshd.setPort(port);
AbstractGeneratorHostKeyProvider hostKeyProvider;
Path hostKeyFile;
if (SecurityUtils.isBouncyCastleRegistered()) {
hostKeyFile = new File("key.pem").toPath();
hostKeyProvider = SecurityUtils.createGeneratorHostKeyProvider(hostKeyFile);
} else {
hostKeyFile = new File("key.ser").toPath();
hostKeyProvider = new SimpleGeneratorHostKeyProvider(hostKeyFile);
}
hostKeyProvider.setAlgorithm(hostKeyType);
if (hostKeySize != 0) {
hostKeyProvider.setKeySize(hostKeySize);
}
List<KeyPair> keys = ValidateUtils.checkNotNullAndNotEmpty(hostKeyProvider.loadKeys(),
"Failed to load keys from %s", hostKeyFile);
KeyPair kp = keys.get(0);
PublicKey pubKey = kp.getPublic();
String keyAlgorithm = pubKey.getAlgorithm();
if ("ECDSA".equalsIgnoreCase(keyAlgorithm)) {
keyAlgorithm = KeyUtils.EC_ALGORITHM;
}
// force re-generation of host key if not same algorithm
if (!Objects.equals(keyAlgorithm, hostKeyType)) {
Files.deleteIfExists(hostKeyFile);
hostKeyProvider.clearLoadedKeys();
}
sshd.setKeyPairProvider(hostKeyProvider);
sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
@Override
public boolean authenticate(String username, String password, ServerSession session) {
return (GenericUtils.length(username) > 0) && username.equals(password);
}
});
sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
sshd.setTcpipForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(new CommandFactory() {
@Override
public Command createCommand(String command) {
return new ProcessShellFactory(GenericUtils.split(command, ' ')).create();
}
}).build());
sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory()));
sshd.start();
Thread.sleep(Long.MAX_VALUE);
}
public static String setupServerBanner(ServerFactoryManager server, Map<String, ?> options) throws IOException {
String filePath = GenericUtils.isEmpty(options) ? null : Objects.toString(options.remove(SshConfigFileReader.BANNER_CONFIG_PROP), null);
if (GenericUtils.length(filePath) > 0) {
if ("none".equals(filePath)) {
return null;
}
Path path = Paths.get(filePath);
long fileSize = Files.size(path);
ValidateUtils.checkTrue(fileSize > 0L, "No banner contents in file=%s", filePath);
StringBuilder sb = new StringBuilder((int) fileSize + Long.SIZE);
try (BufferedReader rdr = new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
sb.append(line).append('\n');
}
}
PropertyResolverUtils.updateProperty(server, ServerFactoryManager.WELCOME_BANNER, sb.toString());
} else {
PropertyResolverUtils.updateProperty(server, ServerFactoryManager.WELCOME_BANNER, "Welcome to SSHD\n");
}
return PropertyResolverUtils.getString(server, ServerFactoryManager.WELCOME_BANNER);
}
}