/*
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Git Development Community nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.spearce.jgit.transport;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import org.spearce.jgit.errors.NoRemoteRepositoryException;
import org.spearce.jgit.errors.TransportException;
import org.spearce.jgit.lib.Repository;
import org.spearce.jgit.util.QuotedString;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
/**
* Transport through an SSH tunnel.
* <p>
* The SSH transport requires the remote side to have Git installed, as the
* transport logs into the remote system and executes a Git helper program on
* the remote side to read (or write) the remote repository's files.
* <p>
* This transport does not support direct SCP style of copying files, as it
* assumes there are Git specific smarts on the remote side to perform object
* enumeration, save file modification and hook execution.
*/
public class TransportGitSsh extends SshTransport implements PackTransport {
static boolean canHandle(final URIish uri) {
if (!uri.isRemote())
return false;
final String scheme = uri.getScheme();
if ("ssh".equals(scheme))
return true;
if ("ssh+git".equals(scheme))
return true;
if ("git+ssh".equals(scheme))
return true;
if (scheme == null && uri.getHost() != null && uri.getPath() != null)
return true;
return false;
}
OutputStream errStream;
TransportGitSsh(final Repository local, final URIish uri) {
super(local, uri);
}
@Override
public FetchConnection openFetch() throws TransportException {
return new SshFetchConnection();
}
@Override
public PushConnection openPush() throws TransportException {
return new SshPushConnection();
}
private static void sqMinimal(final StringBuilder cmd, final String val) {
if (val.matches("^[a-zA-Z0-9._/-]*$")) {
// If the string matches only generally safe characters
// that the shell is not going to evaluate specially we
// should leave the string unquoted. Not all systems
// actually run a shell and over-quoting confuses them
// when it comes to the command name.
//
cmd.append(val);
} else {
sq(cmd, val);
}
}
private static void sqAlways(final StringBuilder cmd, final String val) {
sq(cmd, val);
}
private static void sq(final StringBuilder cmd, final String val) {
if (val.length() > 0)
cmd.append(QuotedString.BOURNE.quote(val));
}
ChannelExec exec(final String exe) throws TransportException {
initSession();
final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0;
try {
final ChannelExec channel = (ChannelExec) sock.openChannel("exec");
String path = uri.getPath();
if (uri.getScheme() != null && uri.getPath().startsWith("/~"))
path = (uri.getPath().substring(1));
final StringBuilder cmd = new StringBuilder();
final int gitspace = exe.indexOf("git ");
if (gitspace >= 0) {
sqMinimal(cmd, exe.substring(0, gitspace + 3));
cmd.append(' ');
sqMinimal(cmd, exe.substring(gitspace + 4));
} else
sqMinimal(cmd, exe);
cmd.append(' ');
sqAlways(cmd, path);
channel.setCommand(cmd.toString());
errStream = createErrorStream();
channel.setErrStream(errStream, true);
channel.connect(tms);
return channel;
} catch (JSchException je) {
throw new TransportException(uri, je.getMessage(), je);
}
}
/**
* @return the error stream for the channel, the stream is used to detect
* specific error reasons for exceptions.
*/
private static OutputStream createErrorStream() {
return new OutputStream() {
private StringBuilder all = new StringBuilder();
private StringBuilder sb = new StringBuilder();
public String toString() {
String r = all.toString();
while (r.endsWith("\n"))
r = r.substring(0, r.length() - 1);
return r;
}
@Override
public void write(final int b) throws IOException {
if (b == '\r') {
return;
}
sb.append((char) b);
if (b == '\n') {
all.append(sb);
sb.setLength(0);
}
}
};
}
NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf) {
String why = errStream.toString();
if (why == null || why.length() == 0)
return nf;
String path = uri.getPath();
if (uri.getScheme() != null && uri.getPath().startsWith("/~"))
path = uri.getPath().substring(1);
final StringBuilder pfx = new StringBuilder();
pfx.append("fatal: ");
sqAlways(pfx, path);
pfx.append(": ");
if (why.startsWith(pfx.toString()))
why = why.substring(pfx.length());
return new NoRemoteRepositoryException(uri, why);
}
// JSch won't let us interrupt writes when we use our InterruptTimer to
// break out of a long-running write operation. To work around that we
// spawn a background thread to shuttle data through a pipe, as we can
// issue an interrupted write out of that. Its slower, so we only use
// this route if there is a timeout.
//
private OutputStream outputStream(ChannelExec channel) throws IOException {
final OutputStream out = channel.getOutputStream();
if (getTimeout() <= 0)
return out;
final PipedInputStream pipeIn = new PipedInputStream();
final CopyThread copyThread = new CopyThread(pipeIn, out);
final PipedOutputStream pipeOut = new PipedOutputStream(pipeIn) {
@Override
public void flush() throws IOException {
super.flush();
copyThread.flush();
}
@Override
public void close() throws IOException {
super.close();
try {
copyThread.join(getTimeout() * 1000);
} catch (InterruptedException e) {
// Just wake early, the thread will terminate anyway.
}
}
};
copyThread.start();
return pipeOut;
}
private static class CopyThread extends Thread {
private final InputStream src;
private final OutputStream dst;
private volatile boolean doFlush;
CopyThread(final InputStream i, final OutputStream o) {
setName(Thread.currentThread().getName() + "-Output");
src = i;
dst = o;
}
void flush() {
if (!doFlush) {
doFlush = true;
interrupt();
}
}
@Override
public void run() {
try {
final byte[] buf = new byte[1024];
for (;;) {
try {
if (doFlush) {
doFlush = false;
dst.flush();
}
final int n;
try {
n = src.read(buf);
} catch (InterruptedIOException wakey) {
continue;
}
if (n < 0)
break;
dst.write(buf, 0, n);
} catch (IOException e) {
break;
}
}
} finally {
try {
src.close();
} catch (IOException e) {
// Ignore IO errors on close
}
try {
dst.close();
} catch (IOException e) {
// Ignore IO errors on close
}
}
}
}
class SshFetchConnection extends BasePackFetchConnection {
private ChannelExec channel;
SshFetchConnection() throws TransportException {
super(TransportGitSsh.this);
try {
channel = exec(getOptionUploadPack());
if (channel.isConnected())
init(channel.getInputStream(), outputStream(channel));
else
throw new TransportException(uri, errStream.toString());
} catch (TransportException err) {
close();
throw err;
} catch (IOException err) {
close();
throw new TransportException(uri,
"remote hung up unexpectedly", err);
}
try {
readAdvertisedRefs();
} catch (NoRemoteRepositoryException notFound) {
throw cleanNotFound(notFound);
}
}
@Override
public void close() {
super.close();
if (channel != null) {
try {
if (channel.isConnected())
channel.disconnect();
} finally {
channel = null;
}
}
}
}
class SshPushConnection extends BasePackPushConnection {
private ChannelExec channel;
SshPushConnection() throws TransportException {
super(TransportGitSsh.this);
try {
channel = exec(getOptionReceivePack());
if (channel.isConnected())
init(channel.getInputStream(), outputStream(channel));
else
throw new TransportException(uri, errStream.toString());
} catch (TransportException err) {
close();
throw err;
} catch (IOException err) {
close();
throw new TransportException(uri,
"remote hung up unexpectedly", err);
}
try {
readAdvertisedRefs();
} catch (NoRemoteRepositoryException notFound) {
throw cleanNotFound(notFound);
}
}
@Override
public void close() {
super.close();
if (channel != null) {
try {
if (channel.isConnected())
channel.disconnect();
} finally {
channel = null;
}
}
}
}
}