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}
|
errLastError=System message for error {0} could not be retrieved, got {1}
|
||||||
errReleaseSharedMemory=Cannot release shared memory: {0} - {1}
|
errReleaseSharedMemory=Cannot release shared memory: {0} - {1}
|
||||||
errUnknown=unknown error
|
errUnknown=unknown error
|
||||||
|
errUnknownIdentityAgent=IdentityAgent ''{0}'' unknown
|
||||||
logErrorLoadLibrary=Cannot load socket library; SSH agent support is switched off
|
logErrorLoadLibrary=Cannot load socket library; SSH agent support is switched off
|
||||||
msgCloseFailed=Cannot close SSH agent socket {0}
|
msgCloseFailed=Cannot close SSH agent socket {0}
|
||||||
msgConnectFailed=Could not connect to SSH agent via 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}
|
msgNoMappedFile=Could not create file mapping: {0} - {1}
|
||||||
msgNoSharedMemory=Could not initialize shared memory: {0} - {1}
|
msgNoSharedMemory=Could not initialize shared memory: {0} - {1}
|
||||||
msgPageantUnavailable=Could not connect to Pageant
|
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}
|
msgShortRead=Short read from SSH agent, expected {0} bytes, got {1} bytes; last read() returned {2}
|
||||||
pageant=Pageant
|
pageant=Pageant
|
||||||
unixDefaultAgent=ssh-agent
|
unixDefaultAgent=ssh-agent
|
||||||
|
winOpenSsh=Win32 OpenSSH
|
||||||
|
|
|
@ -11,11 +11,15 @@
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
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.Connector;
|
||||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
|
||||||
|
import org.eclipse.jgit.util.StringUtils;
|
||||||
import org.eclipse.jgit.util.SystemReader;
|
import org.eclipse.jgit.util.SystemReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,7 +33,20 @@ public class Factory implements ConnectorFactory {
|
||||||
public Connector create(String identityAgent, File homeDir)
|
public Connector create(String identityAgent, File homeDir)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if (SystemReader.getInstance().isWindows()) {
|
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);
|
return new UnixDomainSocketConnector(identityAgent);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +72,11 @@ public String getName() {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Collection<ConnectorDescriptor> getSupportedConnectors() {
|
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
|
@Override
|
||||||
|
|
|
@ -53,6 +53,10 @@ private LibraryHolder() {
|
||||||
kernel = Kernel32.INSTANCE;
|
kernel = Kernel32.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String systemError() {
|
||||||
|
return systemError("[{0}] - {1}"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
String systemError(String pattern) {
|
String systemError(String pattern) {
|
||||||
int lastError = kernel.GetLastError();
|
int lastError = kernel.GetLastError();
|
||||||
String msg;
|
String msg;
|
||||||
|
|
|
@ -31,9 +31,11 @@ public static Texts get() {
|
||||||
/***/ public String errLastError;
|
/***/ public String errLastError;
|
||||||
/***/ public String errReleaseSharedMemory;
|
/***/ public String errReleaseSharedMemory;
|
||||||
/***/ public String errUnknown;
|
/***/ public String errUnknown;
|
||||||
|
/***/ public String errUnknownIdentityAgent;
|
||||||
/***/ public String logErrorLoadLibrary;
|
/***/ public String logErrorLoadLibrary;
|
||||||
/***/ public String msgCloseFailed;
|
/***/ public String msgCloseFailed;
|
||||||
/***/ public String msgConnectFailed;
|
/***/ public String msgConnectFailed;
|
||||||
|
/***/ public String msgConnectPipeFailed;
|
||||||
/***/ public String msgNoMappedFile;
|
/***/ public String msgNoMappedFile;
|
||||||
/***/ public String msgNoSharedMemory;
|
/***/ public String msgNoSharedMemory;
|
||||||
/***/ public String msgPageantUnavailable;
|
/***/ public String msgPageantUnavailable;
|
||||||
|
@ -44,5 +46,6 @@ public static Texts get() {
|
||||||
/***/ public String msgShortRead;
|
/***/ public String msgShortRead;
|
||||||
/***/ public String pageant;
|
/***/ public String pageant;
|
||||||
/***/ public String unixDefaultAgent;
|
/***/ 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;
|
return false;
|
||||||
}
|
}
|
||||||
boolean connected = connector != null && connector.connect();
|
boolean connected;
|
||||||
if (!connected) {
|
try {
|
||||||
if (debugging) {
|
connected = connector != null && connector.connect();
|
||||||
LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$
|
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;
|
return connected;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue