blob: abfbca4b02fa7989474c82a4b602f4dc190e4dd5 [file] [log] [blame]
/*
* 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.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinDef.LPARAM;
import com.sun.jna.platform.win32.WinDef.LRESULT;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.WinUser;
/**
* The {@link PageantLibrary} encapsulates the shared memory access and provides
* a simple pipe abstraction.
*/
public final class PageantLibrary {
private static final Logger LOG = LoggerFactory
.getLogger(PageantLibrary.class);
/** Pageant's "class" and "window name". */
private static final String PAGEANT = "Pageant"; //$NON-NLS-1$
/**
* Magic constant from Pageant; ID for the CopyStruct used in SendMessage.
*
* @see <a href=
* "https://git.tartarus.org/?p=simon/putty.git;a=blob;f=windows/pageant.c;h=0e25cc5d48f0#l33">"random goop"</a>
*/
private static final int PAGEANT_ID = 0x804e_50ba;
/**
* Determines whether Pageant is currently running.
*
* @return {@code true} if Pageant is running, {@code false} otherwise
*/
boolean isPageantAvailable() {
LibraryHolder libs = LibraryHolder.getLibrary();
if (libs == null) {
return false;
}
HWND window = libs.user.FindWindow(PAGEANT, PAGEANT);
return window != null && !window.equals(WinBase.INVALID_HANDLE_VALUE);
}
/**
* An abstraction for a bi-directional pipe.
*/
interface Pipe extends Closeable {
/**
* Send the given message.
*
* @param message
* to send
* @throws IOException
* on errors
*/
void send(byte[] message) throws IOException;
/**
* Reads bytes from the pipe until {@code data} is full.
*
* @param data
* to read
* @throws IOException
* on errors
*/
void receive(byte[] data) throws IOException;
}
/**
* Windows' COPYDATASTRUCT. Must be public for JNA.
*/
public static class CopyStruct extends Structure {
/** Must be set the {@link #PAGEANT_ID}. */
public int dwData = PAGEANT_ID;
/** Data length; number of bytes in {@link #lpData}. */
public long cbData;
/** Points to {@link #cbData} bytes. */
public Pointer lpData;
@Override
protected List<String> getFieldOrder() {
return List.of("dwData", "cbData", "lpData"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
private static class PipeImpl implements Pipe {
private final LibraryHolder libs;
private final HWND window;
private final byte[] name;
private final HANDLE file;
private final Pointer memory;
private long readPos = 0;
PipeImpl(LibraryHolder libs, HWND window, String name, HANDLE file,
Pointer memory) {
this.libs = libs;
this.window = window;
this.name = name.getBytes(StandardCharsets.US_ASCII);
this.file = file;
this.memory = memory;
}
@Override
public void close() throws IOException {
PageantLibrary.close(libs, file, memory, false);
}
private Pointer init(CopyStruct c) {
c.cbData = name.length + 1L;
c.lpData = new Memory(c.cbData);
c.lpData.write(0, name, 0, name.length);
c.lpData.setByte(name.length, (byte) 0);
c.write();
return c.getPointer();
}
@Override
public void send(byte[] message) throws IOException {
memory.write(0, message, 0, message.length);
CopyStruct c = new CopyStruct();
Pointer p = init(c);
LRESULT result = libs.user.SendMessage(window, WinUser.WM_COPYDATA,
null, new LPARAM(Pointer.nativeValue(p)));
if (result == null || result.longValue() == 0) {
throw new IOException(
libs.systemError(Texts.get().msgSendFailed2));
}
}
@Override
public void receive(byte[] data) throws IOException {
// Relies on Pageant handling the request synchronously, i.e.,
// SendMessage() above returning successfully only once Pageant
// has indeed written into the shared memory.
memory.read(readPos, data, 0, data.length);
readPos += data.length;
}
}
/**
* Creates a new {@link Pipe}.
*
* @param name
* for the pipe
* @param maxSize
* maximum size for messages
* @return the {@link Pipe}, or {@code null} if none created
* @throws IOException on errors
*/
Pipe createPipe(String name, int maxSize) throws IOException {
LibraryHolder libs = LibraryHolder.getLibrary();
if (libs == null) {
throw new IllegalStateException("Libraries were not loaded"); //$NON-NLS-1$
}
HWND window = libs.user.FindWindow(PAGEANT, PAGEANT);
if (window == null || window.equals(WinBase.INVALID_HANDLE_VALUE)) {
throw new IOException(Texts.get().msgPageantUnavailable);
}
String fileName = name + libs.kernel.GetCurrentThreadId();
HANDLE file = null;
Pointer sharedMemory = null;
try {
file = libs.kernel.CreateFileMapping(WinBase.INVALID_HANDLE_VALUE,
null, WinNT.PAGE_READWRITE, 0, maxSize, fileName);
if (file == null || file.equals(WinBase.INVALID_HANDLE_VALUE)) {
throw new IOException(
libs.systemError(Texts.get().msgNoMappedFile));
}
sharedMemory = libs.kernel.MapViewOfFile(file,
WinNT.SECTION_MAP_WRITE, 0, 0, 0);
if (sharedMemory == null) {
throw new IOException(
libs.systemError(Texts.get().msgNoSharedMemory));
}
return new PipeImpl(libs, window, fileName, file, sharedMemory);
} catch (IOException e) {
close(libs, file, sharedMemory, true);
throw e;
} catch (Throwable e) {
close(libs, file, sharedMemory, true);
throw new IOException(Texts.get().msgSharedMemoryFailed, e);
}
}
private static void close(LibraryHolder libs, HANDLE file, Pointer memory,
boolean silent) throws IOException {
if (memory != null) {
if (!libs.kernel.UnmapViewOfFile(memory)) {
String msg = libs
.systemError(Texts.get().errReleaseSharedMemory);
if (silent) {
LOG.error(msg);
} else {
throw new IOException(msg);
}
}
}
if (file != null) {
if (!libs.kernel.CloseHandle(file)) {
String msg = libs.systemError(Texts.get().errCloseMappedFile);
if (silent) {
LOG.error(msg);
} else {
throw new IOException(msg);
}
}
}
}
}