2 * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Distribution License v. 1.0 which is available at
6 * https://www.eclipse.org/org/documents/edl-v10.php.
8 * SPDX-License-Identifier: BSD-3-Clause
10 package org.eclipse.jgit.internal.transport.sshd.agent.connector;
12 import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.AF_UNIX;
13 import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.DEFAULT_PROTOCOL;
14 import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.ENV_SSH_AUTH_SOCK;
15 import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.SOCK_STREAM;
16 import static org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixSockets.FD_CLOEXEC;
17 import static org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixSockets.F_SETFD;
19 import java.io.IOException;
20 import java.nio.charset.StandardCharsets;
21 import java.text.MessageFormat;
22 import java.util.Arrays;
23 import java.util.concurrent.atomic.AtomicBoolean;
25 import org.apache.sshd.common.SshException;
26 import org.eclipse.jgit.transport.sshd.agent.AbstractConnector;
27 import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory.ConnectorDescriptor;
28 import org.eclipse.jgit.util.StringUtils;
29 import org.eclipse.jgit.util.SystemReader;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 import com.sun.jna.LastErrorException;
34 import com.sun.jna.Native;
35 import com.sun.jna.platform.unix.LibCAPI;
38 * JNA-based implementation of communication through a Unix domain socket.
40 public class UnixDomainSocketConnector extends AbstractConnector {
43 * {@link ConnectorDescriptor} for the {@link UnixDomainSocketConnector}.
45 public static final ConnectorDescriptor DESCRIPTOR = new ConnectorDescriptor() {
48 public String getIdentityAgent() {
49 return ENV_SSH_AUTH_SOCK;
53 public String getDisplayName() {
54 return Texts.get().unixDefaultAgent;
58 private static final Logger LOG = LoggerFactory
59 .getLogger(UnixDomainSocketConnector.class);
61 private static UnixSockets library;
63 private static boolean libraryLoaded = false;
65 private static synchronized UnixSockets getLibrary() {
69 library = Native.load(UnixSockets.LIBRARY_NAME, UnixSockets.class);
70 } catch (Exception | UnsatisfiedLinkError
71 | NoClassDefFoundError e) {
72 LOG.error(Texts.get().logErrorLoadLibrary, e);
78 private final String socketFile;
80 private AtomicBoolean connected = new AtomicBoolean();
82 private volatile int socketFd = -1;
85 * Creates a new instance.
88 * to use; if {@code null} or empty, use environment variable
91 public UnixDomainSocketConnector(String socketFile) {
93 String file = socketFile;
94 if (StringUtils.isEmptyOrNull(file)) {
95 file = SystemReader.getInstance().getenv(ENV_SSH_AUTH_SOCK);
97 this.socketFile = file;
101 public boolean connect() throws IOException {
102 if (StringUtils.isEmptyOrNull(socketFile)) {
106 synchronized (this) {
107 if (connected.get()) {
110 UnixSockets sockets = getLibrary();
111 if (sockets == null) {
115 fd = sockets.socket(AF_UNIX, SOCK_STREAM, DEFAULT_PROTOCOL);
116 // OS X apparently doesn't have SOCK_CLOEXEC, so we can't set it
117 // atomically. Set it via fcntl, which exists on all systems
118 // we're interested in.
119 sockets.fcntl(fd, F_SETFD, FD_CLOEXEC);
120 Sockets.SockAddr sockAddr = new Sockets.SockAddr(socketFile,
121 StandardCharsets.UTF_8);
122 sockets.connect(fd, sockAddr, sockAddr.size());
124 } catch (LastErrorException e) {
128 } catch (LastErrorException e1) {
132 throw new IOException(MessageFormat
133 .format(Texts.get().msgConnectFailed, socketFile), e);
137 return connected.get();
141 public synchronized void close() throws IOException {
143 if (connected.getAndSet(false) && fd >= 0) {
146 getLibrary().close(fd);
147 } catch (LastErrorException e) {
148 throw new IOException(MessageFormat.format(
149 Texts.get().msgCloseFailed, Integer.toString(fd)), e);
155 public byte[] rpc(byte command, byte[] message) throws IOException {
156 prepareMessage(command, message);
158 if (!connected.get() || fd < 0) {
159 // No translation, internal error
160 throw new IllegalStateException("Not connected to SSH agent"); //$NON-NLS-1$
162 writeFully(fd, message);
163 // Now receive the reply
164 byte[] lengthBuf = new byte[4];
165 readFully(fd, lengthBuf);
166 int length = toLength(command, lengthBuf);
167 byte[] payload = new byte[length];
168 readFully(fd, payload);
172 private void writeFully(int fd, byte[] message) throws IOException {
173 int toWrite = message.length;
175 byte[] buf = message;
176 while (toWrite > 0) {
177 int written = getLibrary()
178 .write(fd, buf, new LibCAPI.size_t(buf.length))
181 throw new IOException(
182 MessageFormat.format(Texts.get().msgSendFailed,
183 Integer.toString(message.length),
184 Integer.toString(toWrite)));
187 if (written > 0 && toWrite > 0) {
188 buf = Arrays.copyOfRange(buf, written, buf.length);
191 } catch (LastErrorException e) {
192 throw new IOException(
193 MessageFormat.format(Texts.get().msgSendFailed,
194 Integer.toString(message.length),
195 Integer.toString(toWrite)),
200 private void readFully(int fd, byte[] data) throws IOException {
203 while (offset < data.length
204 && (n = read(fd, data, offset, data.length - offset)) > 0) {
207 if (offset < data.length) {
208 throw new SshException(
209 MessageFormat.format(Texts.get().msgShortRead,
210 Integer.toString(data.length),
211 Integer.toString(offset), Integer.toString(n)));
215 private int read(int fd, byte[] buffer, int offset, int length)
218 LibCAPI.size_t toRead = new LibCAPI.size_t(length);
220 return getLibrary().read(fd, buffer, toRead).intValue();
222 byte[] data = new byte[length];
223 int read = getLibrary().read(fd, data, toRead).intValue();
225 System.arraycopy(data, 0, buffer, offset, read);
228 } catch (LastErrorException e) {
229 throw new IOException(
230 MessageFormat.format(Texts.get().msgReadFailed,
231 Integer.toString(length)),