diff --git a/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties b/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties index 6fce08366..a3b4e91ca 100644 --- a/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties +++ b/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties @@ -2,9 +2,11 @@ errCloseMappedFile=Cannot close mapped file: {0} - {1} errLastError=System message for error {0} could not be retrieved, got {1} errReleaseSharedMemory=Cannot release shared memory: {0} - {1} errUnknown=unknown error +errUnknownIdentityAgent=IdentityAgent ''{0}'' unknown logErrorLoadLibrary=Cannot load socket library; SSH agent support is switched off msgCloseFailed=Cannot close SSH agent socket {0} msgConnectFailed=Could not connect to SSH agent via socket ''{0}'' +msgConnectPipeFailed=Could not connect to SSH agent via pipe ''{0}'' msgNoMappedFile=Could not create file mapping: {0} - {1} msgNoSharedMemory=Could not initialize shared memory: {0} - {1} msgPageantUnavailable=Could not connect to Pageant @@ -15,3 +17,4 @@ msgSharedMemoryFailed=Could not set up shared memory for communicating with Page msgShortRead=Short read from SSH agent, expected {0} bytes, got {1} bytes; last read() returned {2} pageant=Pageant unixDefaultAgent=ssh-agent +winOpenSsh=Win32 OpenSSH diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java index d7409b0c3..1cee1be13 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java @@ -11,11 +11,15 @@ import java.io.File; import java.io.IOException; +import java.text.MessageFormat; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Locale; import org.eclipse.jgit.transport.sshd.agent.Connector; import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; /** @@ -29,7 +33,20 @@ public class Factory implements ConnectorFactory { public Connector create(String identityAgent, File homeDir) throws IOException { if (SystemReader.getInstance().isWindows()) { - return new PageantConnector(); + if (StringUtils.isEmptyOrNull(identityAgent)) { + // Default. + return new PageantConnector(); + } + String winPath = identityAgent.replace('/', '\\'); + if (PageantConnector.DESCRIPTOR.getIdentityAgent() + .equalsIgnoreCase(winPath)) { + return new PageantConnector(); + } + if (winPath.toLowerCase(Locale.ROOT).startsWith("\\\\.\\pipe\\")) { //$NON-NLS-1$ + return new WinPipeConnector(winPath); + } + throw new IOException(MessageFormat.format( + Texts.get().errUnknownIdentityAgent, identityAgent)); } return new UnixDomainSocketConnector(identityAgent); } @@ -55,7 +72,11 @@ public String getName() { */ @Override public Collection getSupportedConnectors() { - return Collections.singleton(getDefaultConnector()); + if (SystemReader.getInstance().isWindows()) { + return List.of(PageantConnector.DESCRIPTOR, + WinPipeConnector.DESCRIPTOR); + } + return Collections.singleton(UnixDomainSocketConnector.DESCRIPTOR); } @Override diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java index b09b55f81..0a592d050 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java @@ -53,6 +53,10 @@ private LibraryHolder() { kernel = Kernel32.INSTANCE; } + String systemError() { + return systemError("[{0}] - {1}"); //$NON-NLS-1$ + } + String systemError(String pattern) { int lastError = kernel.GetLastError(); String msg; diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java index fb45b30dd..f387c76ad 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java @@ -31,9 +31,11 @@ public static Texts get() { /***/ public String errLastError; /***/ public String errReleaseSharedMemory; /***/ public String errUnknown; + /***/ public String errUnknownIdentityAgent; /***/ public String logErrorLoadLibrary; /***/ public String msgCloseFailed; /***/ public String msgConnectFailed; + /***/ public String msgConnectPipeFailed; /***/ public String msgNoMappedFile; /***/ public String msgNoSharedMemory; /***/ public String msgPageantUnavailable; @@ -44,5 +46,6 @@ public static Texts get() { /***/ public String msgShortRead; /***/ public String pageant; /***/ public String unixDefaultAgent; + /***/ public String winOpenSsh; } diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java new file mode 100644 index 000000000..7bad90f24 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2021, Thomas Wolf and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd.agent.connector; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.common.SshException; +import org.eclipse.jgit.transport.sshd.agent.AbstractConnector; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory.ConnectorDescriptor; +import org.eclipse.jgit.util.StringUtils; + +import com.sun.jna.LastErrorException; +import com.sun.jna.platform.win32.WinBase; +import com.sun.jna.platform.win32.WinError; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.ptr.IntByReference; + +/** + * A connector based on JNA using Windows' named pipes to communicate with an + * ssh agent. This is used by Microsoft's Win32-OpenSSH port. + */ +public class WinPipeConnector extends AbstractConnector { + + // Pipe names are, like other file names, case-insensitive on Windows. + private static final String CANONICAL_PIPE_NAME = "\\\\.\\pipe\\openssh-ssh-agent"; //$NON-NLS-1$ + + /** + * {@link ConnectorDescriptor} for the {@link PageantConnector}. + */ + public static final ConnectorDescriptor DESCRIPTOR = new ConnectorDescriptor() { + + @Override + public String getIdentityAgent() { + return CANONICAL_PIPE_NAME; + } + + @Override + public String getDisplayName() { + return Texts.get().winOpenSsh; + } + }; + + private static final int FILE_SHARE_NONE = 0; + + private static final int FILE_ATTRIBUTE_NONE = 0; + + private final String pipeName; + + private final AtomicBoolean connected = new AtomicBoolean(); + + // It's a byte pipe, so the normal Windows file mechanisms can be used. + // Would one of the standard Java File I/O abstractions work? + private volatile HANDLE fileHandle; + + /** + * Creates a {@link WinPipeConnector} for the given named pipe. + * + * @param pipeName + * to connect to + */ + public WinPipeConnector(String pipeName) { + this.pipeName = pipeName.replace('/', '\\'); + } + + @Override + public boolean connect() throws IOException { + if (StringUtils.isEmptyOrNull(pipeName)) { + return false; + } + HANDLE file = fileHandle; + synchronized (this) { + if (connected.get()) { + return true; + } + LibraryHolder libs = LibraryHolder.getLibrary(); + if (libs == null) { + return false; + } + file = libs.kernel.CreateFile(pipeName, + WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, FILE_SHARE_NONE, + null, WinNT.OPEN_EXISTING, FILE_ATTRIBUTE_NONE, null); + if (file == null || file == WinBase.INVALID_HANDLE_VALUE) { + int errorCode = libs.kernel.GetLastError(); + if (errorCode == WinError.ERROR_FILE_NOT_FOUND + && CANONICAL_PIPE_NAME.equalsIgnoreCase(pipeName)) { + // OpenSSH agent not running. Don't throw. + return false; + } + LastErrorException cause = new LastErrorException( + libs.systemError()); + throw new IOException(MessageFormat + .format(Texts.get().msgConnectPipeFailed, pipeName), + cause); + } + connected.set(true); + } + fileHandle = file; + return connected.get(); + } + + @Override + public synchronized void close() throws IOException { + HANDLE file = fileHandle; + if (connected.getAndSet(false) && fileHandle != null) { + fileHandle = null; + LibraryHolder libs = LibraryHolder.getLibrary(); + boolean success = libs.kernel.CloseHandle(file); + if (!success) { + LastErrorException cause = new LastErrorException( + libs.systemError()); + throw new IOException(MessageFormat + .format(Texts.get().msgCloseFailed, pipeName), cause); + } + } + } + + @Override + public byte[] rpc(byte command, byte[] message) throws IOException { + prepareMessage(command, message); + HANDLE file = fileHandle; + if (!connected.get() || file == null) { + // No translation, internal error + throw new IllegalStateException("Not connected to SSH agent"); //$NON-NLS-1$ + } + LibraryHolder libs = LibraryHolder.getLibrary(); + writeFully(libs, file, message); + // Now receive the reply + byte[] lengthBuf = new byte[4]; + readFully(libs, file, lengthBuf); + int length = toLength(command, lengthBuf); + byte[] payload = new byte[length]; + readFully(libs, file, payload); + return payload; + } + + private void writeFully(LibraryHolder libs, HANDLE file, byte[] message) + throws IOException { + byte[] buf = message; + int toWrite = buf.length; + try { + while (toWrite > 0) { + IntByReference written = new IntByReference(); + boolean success = libs.kernel.WriteFile(file, buf, buf.length, + written, null); + if (!success) { + throw new LastErrorException(libs.systemError()); + } + int actuallyWritten = written.getValue(); + toWrite -= actuallyWritten; + if (actuallyWritten > 0 && toWrite > 0) { + buf = Arrays.copyOfRange(buf, actuallyWritten, buf.length); + } + } + } catch (LastErrorException e) { + throw new IOException(MessageFormat.format( + Texts.get().msgSendFailed, Integer.toString(message.length), + Integer.toString(toWrite)), e); + } + } + + private void readFully(LibraryHolder libs, HANDLE file, byte[] data) + throws IOException { + int n = 0; + int offset = 0; + while (offset < data.length && (n = read(libs, file, data, offset, + data.length - offset)) > 0) { + offset += n; + } + if (offset < data.length) { + throw new SshException(MessageFormat.format( + Texts.get().msgShortRead, Integer.toString(data.length), + Integer.toString(offset), Integer.toString(n))); + } + } + + private int read(LibraryHolder libs, HANDLE file, byte[] buffer, int offset, + int length) throws IOException { + try { + int toRead = length; + IntByReference read = new IntByReference(); + if (offset == 0) { + boolean success = libs.kernel.ReadFile(file, buffer, toRead, + read, null); + if (!success) { + throw new LastErrorException(libs.systemError()); + } + return read.getValue(); + } + byte[] data = new byte[length]; + boolean success = libs.kernel.ReadFile(file, buffer, toRead, read, + null); + if (!success) { + throw new LastErrorException(libs.systemError()); + } + int actuallyRead = read.getValue(); + if (actuallyRead > 0) { + System.arraycopy(data, 0, buffer, offset, actuallyRead); + } + return actuallyRead; + } catch (LastErrorException e) { + throw new IOException(MessageFormat.format( + Texts.get().msgReadFailed, Integer.toString(length)), e); + } + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java index 692fb9360..13ca351ea 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java @@ -73,11 +73,18 @@ private boolean open(boolean debugging) throws IOException { } return false; } - boolean connected = connector != null && connector.connect(); - if (!connected) { - if (debugging) { - LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$ + boolean connected; + try { + connected = connector != null && connector.connect(); + if (!connected && debugging) { + LOG.debug("No SSH agent"); //$NON-NLS-1$ } + } catch (IOException e) { + // Agent not running? + if (debugging) { + LOG.debug("No SSH agent", e); //$NON-NLS-1$ + } + throw e; } return connected; }