/*
* 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.karaf.shell.ssh;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.security.PrivilegedAction;
import java.util.Map;
import javax.security.auth.Subject;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.api.console.SessionFactory;
import org.apache.karaf.shell.api.console.Terminal;
import org.apache.karaf.shell.support.ShellUtil;
import org.apache.karaf.util.jaas.JaasHelper;
import org.apache.sshd.common.Factory;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
/**
* SSHD {@link org.apache.sshd.server.Command} factory which provides access to
* Shell.
*/
public class ShellFactoryImpl implements Factory<Command> {
private SessionFactory sessionFactory;
public ShellFactoryImpl(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public Command create() {
return new ShellImpl();
}
public class ShellImpl implements Command, SessionAware {
private InputStream in;
private OutputStream out;
private OutputStream err;
private ExitCallback callback;
private ServerSession session;
private Session shell;
private SshTerminal terminal;
private boolean closed;
public void setInputStream(final InputStream in) {
this.in = in;
}
public void setOutputStream(final OutputStream out) {
this.out = out;
}
public void setErrorStream(final OutputStream err) {
this.err = err;
}
public void setExitCallback(ExitCallback callback) {
this.callback = callback;
}
public void setSession(ServerSession session) {
this.session = session;
}
public void start(final Environment env) throws IOException {
try {
final Subject subject = ShellImpl.this.session != null ? ShellImpl.this.session
.getAttribute(KarafJaasAuthenticator.SUBJECT_ATTRIBUTE_KEY) : null;
String encoding = getEncoding(env);
final PrintStream pout = out instanceof PrintStream ? (PrintStream) out : new PrintStream(out, true, encoding);
final PrintStream perr = err instanceof PrintStream ? (PrintStream) err : out == err ? pout : new PrintStream(err, true, encoding);
terminal = new SshTerminal(env, in, pout);
shell = sessionFactory.create(in,
pout, perr, terminal, encoding, this::destroy);
for (Map.Entry<String, String> e : env.getEnv().entrySet()) {
shell.put(e.getKey(), e.getValue());
}
JaasHelper.runAs(subject, () ->
new Thread(shell, "Karaf ssh console user " + ShellUtil.getCurrentUserName()).start());
} catch (Exception e) {
throw (IOException) new IOException("Unable to start shell").initCause(e);
}
}
public void destroy() {
if (!closed) {
closed = true;
flush(out, err);
close(in, out, err);
callback.onExit(0);
}
}
}
/**
* Get the default encoding. Will first look at the LC_CTYPE environment variable, then the input.encoding
* system property, then the default charset according to the JVM.
*
* @return The default encoding to use when none is specified.
*/
public static String getEncoding(Environment env) {
// LC_CTYPE is usually in the form en_US.UTF-8
String ctype = env.getEnv().getOrDefault("LC_TYPE", System.getenv("LC_CTYPE"));
String envEncoding = extractEncodingFromCtype(ctype);
if (envEncoding != null) {
return envEncoding;
}
return System.getProperty("input.encoding", Charset.defaultCharset().name());
}
/**
* Parses the LC_CTYPE value to extract the encoding according to the POSIX standard, which says that the LC_CTYPE
* environment variable may be of the format <code>[language[_territory][.codeset][@modifier]]</code>
*
* @param ctype The ctype to parse, may be null
* @return The encoding, if one was present, otherwise null
*/
static String extractEncodingFromCtype(String ctype) {
if (ctype != null && ctype.indexOf('.') > 0) {
String encodingAndModifier = ctype.substring(ctype.indexOf('.') + 1);
if (encodingAndModifier.indexOf('@') > 0) {
return encodingAndModifier.substring(0, encodingAndModifier.indexOf('@'));
} else {
return encodingAndModifier;
}
}
return null;
}
private static void flush(OutputStream... streams) {
for (OutputStream s : streams) {
try {
s.flush();
} catch (IOException e) {
// Ignore
}
}
}
private static void close(Closeable... closeables) {
for (Closeable c : closeables) {
try {
c.close();
} catch (Exception e) {
// Ignore
}
}
}
}