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.util.StringUtils;
28 import org.eclipse.jgit.util.SystemReader;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import com.sun.jna.LastErrorException;
33 import com.sun.jna.Native;
34 import com.sun.jna.platform.unix.LibCAPI;
37 * JNA-based implementation of communication through a Unix domain socket.
39 public class UnixDomainSocketConnector extends AbstractConnector {
41 private static final Logger LOG = LoggerFactory
42 .getLogger(UnixDomainSocketConnector.class);
44 private static UnixSockets library;
46 private static boolean libraryLoaded = false;
48 private static synchronized UnixSockets getLibrary() {
52 library = Native.load(UnixSockets.LIBRARY_NAME, UnixSockets.class);
53 } catch (Exception | UnsatisfiedLinkError
54 | NoClassDefFoundError e) {
55 LOG.error(Texts.get().logErrorLoadLibrary, e);
61 private final String socketFile;
63 private AtomicBoolean connected = new AtomicBoolean();
65 private volatile int socketFd = -1;
68 * Creates a new instance.
71 * to use; if {@code null} or empty, use environment variable
74 public UnixDomainSocketConnector(String socketFile) {
76 String file = socketFile;
77 if (StringUtils.isEmptyOrNull(file)) {
78 file = SystemReader.getInstance().getenv(ENV_SSH_AUTH_SOCK);
80 this.socketFile = file;
84 public boolean connect() throws IOException {
85 if (StringUtils.isEmptyOrNull(socketFile)) {
90 if (connected.get()) {
93 UnixSockets sockets = getLibrary();
94 if (sockets == null) {
98 fd = sockets.socket(AF_UNIX, SOCK_STREAM, DEFAULT_PROTOCOL);
99 // OS X apparently doesn't have SOCK_CLOEXEC, so we can't set it
100 // atomically. Set it via fcntl, which exists on all systems
101 // we're interested in.
102 sockets.fcntl(fd, F_SETFD, FD_CLOEXEC);
103 Sockets.SockAddr sockAddr = new Sockets.SockAddr(socketFile,
104 StandardCharsets.UTF_8);
105 sockets.connect(fd, sockAddr, sockAddr.size());
107 } catch (LastErrorException e) {
111 } catch (LastErrorException e1) {
115 throw new IOException(MessageFormat
116 .format(Texts.get().msgConnectFailed, socketFile), e);
120 return connected.get();
124 public synchronized void close() throws IOException {
126 if (connected.getAndSet(false) && fd >= 0) {
129 getLibrary().close(fd);
130 } catch (LastErrorException e) {
131 throw new IOException(MessageFormat.format(
132 Texts.get().msgCloseFailed, Integer.toString(fd)), e);
138 public byte[] rpc(byte command, byte[] message) throws IOException {
139 prepareMessage(command, message);
141 if (!connected.get() || fd < 0) {
142 // No translation, internal error
143 throw new IllegalStateException("Not connected to SSH agent"); //$NON-NLS-1$
145 writeFully(fd, message);
146 // Now receive the reply
147 byte[] lengthBuf = new byte[4];
148 readFully(fd, lengthBuf);
149 int length = toLength(command, lengthBuf);
150 byte[] payload = new byte[length];
151 readFully(fd, payload);
155 private void writeFully(int fd, byte[] message) throws IOException {
156 int toWrite = message.length;
158 byte[] buf = message;
159 while (toWrite > 0) {
160 int written = getLibrary()
161 .write(fd, buf, new LibCAPI.size_t(buf.length))
164 throw new IOException(
165 MessageFormat.format(Texts.get().msgSendFailed,
166 Integer.toString(message.length),
167 Integer.toString(toWrite)));
170 if (written > 0 && toWrite > 0) {
171 buf = Arrays.copyOfRange(buf, written, buf.length);
174 } catch (LastErrorException e) {
175 throw new IOException(
176 MessageFormat.format(Texts.get().msgSendFailed,
177 Integer.toString(message.length),
178 Integer.toString(toWrite)),
183 private void readFully(int fd, byte[] data) throws IOException {
186 while (offset < data.length
187 && (n = read(fd, data, offset, data.length - offset)) > 0) {
190 if (offset < data.length) {
191 throw new SshException(
192 MessageFormat.format(Texts.get().msgShortRead,
193 Integer.toString(data.length),
194 Integer.toString(offset), Integer.toString(n)));
198 private int read(int fd, byte[] buffer, int offset, int length)
201 LibCAPI.size_t toRead = new LibCAPI.size_t(length);
203 return getLibrary().read(fd, buffer, toRead).intValue();
205 byte[] data = new byte[length];
206 int read = getLibrary().read(fd, data, toRead).intValue();
208 System.arraycopy(data, 0, buffer, offset, read);
211 } catch (LastErrorException e) {
212 throw new IOException(
213 MessageFormat.format(Texts.get().msgReadFailed,
214 Integer.toString(length)),