/* * 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.shell; import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.EnumSet; import java.util.Map; import org.apache.sshd.common.Factory; import org.apache.sshd.common.util.Buffer; import org.apache.sshd.common.util.LogUtils; import org.apache.sshd.server.Command; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * A {@link Factory} of {@link Command} that will create a new process and bridge * the streams. * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public class ProcessShellFactory implements Factory<Command> { public enum TtyOptions { Echo, INlCr, ICrNl, ONlCr, OCrNl } private static final Log LOG = LogFactory.getLog(ProcessShellFactory.class); private String[] command; private EnumSet<TtyOptions> ttyOptions; public ProcessShellFactory() { } public ProcessShellFactory(String[] command) { this(command, EnumSet.noneOf(TtyOptions.class)); } public ProcessShellFactory(String[] command, EnumSet<TtyOptions> ttyOptions) { this.command = command; this.ttyOptions = ttyOptions; } public String[] getCommand() { return command; } public void setCommand(String[] command) { this.command = command; } public Command create() { return new InvertedShellWrapper(new ProcessShell()); } public class ProcessShell implements InvertedShell { private Process process; private TtyFilterOutputStream in; private TtyFilterInputStream out; private TtyFilterInputStream err; public void start(Map<String,String> env) throws IOException { String[] cmds = new String[command.length]; for (int i = 0; i < cmds.length; i++) { if ("$USER".equals(command[i])) { cmds[i] = env.get("USER"); } else { cmds[i] = command[i]; } } ProcessBuilder builder = new ProcessBuilder(cmds); if (env != null) { builder.environment().putAll(env); } LogUtils.info(LOG,"Starting shell with command: '{0}' and env: {1}", builder.command(), builder.environment()); process = builder.start(); out = new TtyFilterInputStream(process.getInputStream()); err = new TtyFilterInputStream(process.getErrorStream()); in = new TtyFilterOutputStream(process.getOutputStream(), err); } public OutputStream getInputStream() { return in; } public InputStream getOutputStream() { return out; } public InputStream getErrorStream() { return err; } public boolean isAlive() { try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { return true; } } public int exitValue() { return process.exitValue(); } public void destroy() { process.destroy(); } protected class TtyFilterInputStream extends FilterInputStream { private Buffer buffer; private int lastChar; public TtyFilterInputStream(InputStream in) { super(in); buffer = new Buffer(32); } synchronized void write(int c) { buffer.putByte((byte) c); } synchronized void write(byte[] buf, int off, int len) { buffer.putBytes(buf, off, len); } @Override public int available() throws IOException { return super.available() + buffer.available(); } @Override public synchronized int read() throws IOException { int c; if (buffer.available() > 0) { c = buffer.getByte(); buffer.compact(); } else { c = super.read(); } if (c == '\n' && ttyOptions.contains(TtyOptions.ONlCr) && lastChar != '\r') { c = '\r'; Buffer buf = new Buffer(); buf.putByte((byte) '\n'); buf.putBuffer(buffer); buffer = buf; } else if (c == '\r' && ttyOptions.contains(TtyOptions.OCrNl)) { c = '\n'; } lastChar = c; return c; } @Override public synchronized int read(byte[] b, int off, int len) throws IOException { if (buffer.available() == 0) { int nb = super.read(b, off, len); buffer.putRawBytes(b, off, nb); } int nb = 0; while (nb < len && buffer.available() > 0) { b[off + nb++] = (byte) read(); } return nb; } } protected class TtyFilterOutputStream extends FilterOutputStream { private TtyFilterInputStream echo; public TtyFilterOutputStream(OutputStream out, TtyFilterInputStream echo) { super(out); this.echo = echo; } @Override public void write(int c) throws IOException { if (c == '\n' && ttyOptions.contains(TtyOptions.INlCr)) { c = '\r'; } else if (c == '\r' && ttyOptions.contains(TtyOptions.ICrNl)) { c = '\n'; } super.write(c); if (ttyOptions.contains(TtyOptions.Echo)) { echo.write(c); } } @Override public void write(byte[] b, int off, int len) throws IOException { for (int i = off; i < len; i++) { write(b[i]); } } } } }