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:
parent
071084818c
commit
e0281c5adb
|
@ -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
|
||||
|
|
|
@ -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,8 +33,21 @@ public class Factory implements ConnectorFactory {
|
|||
public Connector create(String identityAgent, File homeDir)
|
||||
throws IOException {
|
||||
if (SystemReader.getInstance().isWindows()) {
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue