sshd: Connector for the Win32-OpenSSH SSH agent

Win32-OpenSSH uses a named Windows pipe for communication. Implement
a connector for this mechanism using JNA. Choose the appropriate
connector based on the setting of the 'identityAgent' parameter.

Bug: 577053
Change-Id: I205f07fb33654aa18ca5db92706e65544ce38641
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2021-12-27 21:39:23 +01:00
parent 071084818c
commit e0281c5adb
6 changed files with 260 additions and 6 deletions

View File

@ -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

View File

@ -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<ConnectorDescriptor> getSupportedConnectors() {
return Collections.singleton(getDefaultConnector());
if (SystemReader.getInstance().isWindows()) {
return List.of(PageantConnector.DESCRIPTOR,
WinPipeConnector.DESCRIPTOR);
}
return Collections.singleton(UnixDomainSocketConnector.DESCRIPTOR);
}
@Override

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,216 @@
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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);
}
}
}

View File

@ -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;
}