net.oneandone.sushi.fs.ssh.SshFilesystem Maven / Gradle / Ivy
/**
* Copyright 1&1 Internet AG, https://github.com/1and1/
*
* Licensed 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 net.oneandone.sushi.fs.ssh;
import com.jcraft.jsch.Identity;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import net.oneandone.sushi.fs.Features;
import net.oneandone.sushi.fs.Filesystem;
import net.oneandone.sushi.fs.Node;
import net.oneandone.sushi.fs.NodeInstantiationException;
import net.oneandone.sushi.fs.World;
import net.oneandone.sushi.util.NetRc;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
/**
* Nodes accessible via sftp.
*
* You'll usually have a default identity, protected with a passphrase, available in ssh-agent (either automatically
* after login or by explicit ssh-add calls).
*
* If ssh-agent is not accessible (because the optional jsch-agent dependencies are missing) you have the following
* - less attractive - options:
* 1) use an identity that's not protected with a passphrase
* 2) otherwise:
* a) explicitly call addDefaultIdentity with the respective passphrase
* b) store the passphrase in ~/.ssh/passphrase
*
* Uses Jsch: http://www.jcraft.com/jsch/
* See also: http://tools.ietf.org/id/draft-ietf-secsh-filexfer-13.txt
*/
public class SshFilesystem extends Filesystem {
/** @param trySshAgent disable this if your ssh agent is configured, but you don't want to use it. */
public static JSch jsch(boolean trySshAgent) throws IOException {
JSch jsch;
jsch = new JSch();
if (trySshAgent) {
try {
SshAgent.configure(jsch);
} catch (NoClassDefFoundError e) {
// ok -- we have no ssh-agent dependencies
}
}
jsch.setHostKeyRepository(new AcceptAllHostKeyRepository());
return jsch;
}
private int defaultTimeout;
private final JSch jsch;
public SshFilesystem(World world, String name, boolean trySshAgent) throws IOException {
this(world, name, jsch(trySshAgent));
this.defaultTimeout = 0;
}
public SshFilesystem(World world, String name, JSch jsch) {
super(world, new Features(true, true, true, true, false, false, true), name);
this.defaultTimeout = 0;
this.jsch = jsch;
}
public Session connect(String host, int port, String user, String password, int timeout) throws JSchException {
Session session;
session = jsch.getSession(user, host, port);
if (password != null) {
session.setPassword(password);
}
session.connect(timeout);
return session;
}
/** millis */
public void setDefaultTimeout(int defaultTimeout) {
this.defaultTimeout = defaultTimeout;
}
/** millis */
public int getDefaultTimeout() {
return defaultTimeout;
}
public JSch getJSch() {
return jsch;
}
@Override
public SshNode node(URI uri, Object extra) throws NodeInstantiationException {
if (extra != null) {
throw new NodeInstantiationException(uri, "unexpected extra argument: " + extra);
}
checkHierarchical(uri);
try {
return root(uri.getAuthority()).node(getCheckedPath(uri), null);
} catch (JSchException | IOException e) {
throw new NodeInstantiationException(uri, "cannot create root", e);
}
}
public SshRoot localhostRoot() throws JSchException, IOException {
return root("localhost", getWorld().getWorking().getName(), null);
}
public SshRoot root(String authority) throws JSchException, IOException {
return root(authority, defaultTimeout);
}
/**
* @param authority Allowed schemas: host and user@host, while user can be user:password and host can be host:port
*/
public SshRoot root(String authority, int timeout) throws JSchException, IOException {
int idx;
String host;
int port;
String user;
String password;
// split user@host
host = authority;
idx = host.indexOf('@');
if (idx == -1) {
user = null;
password = null;
} else {
user = host.substring(0, idx);
host = host.substring(idx + 1);
// split user:password
idx = user.indexOf(':');
if (idx == -1) {
password = null;
} else {
password = user.substring(idx + 1);
user = user.substring(0, idx);
}
}
// split host:port
idx = host.indexOf(':');
if (idx == -1) {
port = SshRoot.DEFAULT_PORT;
} else {
try {
port = Integer.parseInt(host.substring(idx + 1));
} catch (NumberFormatException e) {
throw new JSchException("Invalid port number " + host.substring(idx + 1));
}
host = host.substring(0, idx);
}
return root(host, port, user, password, timeout);
}
public SshRoot root(String host, String user, String password) throws JSchException, IOException {
return root(host, user, password, defaultTimeout);
}
public SshRoot root(String host, String user, String password, int timeout) throws JSchException, IOException {
return root(host, SshRoot.DEFAULT_PORT, user, password, timeout);
}
/** @param user null to use current user */
public SshRoot root(String host, int port, String user, String password, int timeout) throws JSchException, IOException {
NetRc.Authenticator authenticator;
if (user == null) {
authenticator = getWorld().getNetRc().getAuthenticator(host);
if (authenticator == null) {
user = getWorld().getHome().getName();
password = null;
} else {
user = authenticator.getUser();
password = authenticator.getPass();
}
}
addDefaultIdentityOpt();
return new SshRoot(this, host, port, user, password, timeout);
}
//--
/** adds the default identity if the identity repository is empty */
public void addDefaultIdentityOpt() throws IOException, JSchException {
if (jsch.getIdentityNames().isEmpty()) {
addDefaultIdentity();
}
}
public void addDefaultIdentity() throws IOException, JSchException {
addDefaultIdentity(null);
}
/** @param passphrase null to try to load passphrase from ~/.ssh/passphrase file */
public void addDefaultIdentity(String passphrase) throws IOException, JSchException {
Node dir;
Node file;
Node key;
dir = getWorld().getHome().join(".ssh");
file = dir.join("passphrase");
if (passphrase == null && file.exists()) {
passphrase = file.readString().trim();
}
key = dir.join("id_dsa");
if (!key.exists()) {
key = dir.join("id_rsa");
if (!key.exists()) {
key = dir.join("identity");
}
}
if (!key.isFile()) {
throw new IOException("private key not found: " + key);
}
addIdentity(key, passphrase);
}
/**
* Core method to actually add an identity. Identity is a private/public key pair.
* Identities are "not interactive" - this method reports missing passphrases or when
* a passphrase is specified for an identity that does not need one.
*
* @param passphrase null of none
*/
public void addIdentity(Node privateKey, String passphrase) throws IOException, JSchException {
Identity identity;
Throwable te;
Class> clz;
Method m;
byte[] bytes;
bytes = privateKey.readBytes();
// CAUTION: I cannot use
// jsch.addIdentity("foo", null, null, null);
// because in jsch 1.48, there's no way to obtain the resulting identity and the identity.setPassphrase
// result.
try {
clz = Class.forName("com.jcraft.jsch.IdentityFile");
m = clz.getDeclaredMethod("newInstance", String.class, byte[].class, byte[].class, JSch.class);
m.setAccessible(true);
identity = (Identity) m.invoke(null, privateKey.toString(), Arrays.copyOf(bytes, bytes.length), null, jsch);
} catch (InvocationTargetException e) {
te = e.getTargetException();
if (te instanceof JSchException) {
throw (JSchException) te;
} else if (te instanceof IOException) {
throw (IOException) te;
} else {
throw new IllegalStateException(e);
}
} catch (Exception e) {
throw new RuntimeException("TODO", e);
}
if (passphrase != null) {
if (!identity.isEncrypted()) {
throw new JSchException("unexpected passphrase");
}
if (!identity.setPassphrase(passphrase.getBytes())) {
throw new JSchException("invalid passphrase");
}
} else {
if (!identity.setPassphrase(null)) {
throw new JSchException("missing passphrase");
}
}
jsch.removeIdentity(identity);
jsch.addIdentity(identity, null);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy