]> source.dussan.org Git - jgit.git/blob
3b75f3a7da0bf55a798d14d6215d77d9c3bce523
[jgit.git] /
1 /*
2  * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
3  *
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.
7  *
8  * SPDX-License-Identifier: BSD-3-Clause
9  */
10 package org.eclipse.jgit.internal.transport.sshd.agent.connector;
11
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;
18
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;
24
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;
32
33 import com.sun.jna.LastErrorException;
34 import com.sun.jna.Native;
35 import com.sun.jna.platform.unix.LibCAPI;
36
37 /**
38  * JNA-based implementation of communication through a Unix domain socket.
39  */
40 public class UnixDomainSocketConnector extends AbstractConnector {
41
42         /**
43          * {@link ConnectorDescriptor} for the {@link UnixDomainSocketConnector}.
44          */
45         public static final ConnectorDescriptor DESCRIPTOR = new ConnectorDescriptor() {
46
47                 @Override
48                 public String getIdentityAgent() {
49                         return ENV_SSH_AUTH_SOCK;
50                 }
51
52                 @Override
53                 public String getDisplayName() {
54                         return Texts.get().unixDefaultAgent;
55                 }
56         };
57
58         private static final Logger LOG = LoggerFactory
59                         .getLogger(UnixDomainSocketConnector.class);
60
61         private static UnixSockets library;
62
63         private static boolean libraryLoaded = false;
64
65         private static synchronized UnixSockets getLibrary() {
66                 if (!libraryLoaded) {
67                         libraryLoaded = true;
68                         try {
69                                 library = Native.load(UnixSockets.LIBRARY_NAME, UnixSockets.class);
70                         } catch (Exception | UnsatisfiedLinkError
71                                         | NoClassDefFoundError e) {
72                                 LOG.error(Texts.get().logErrorLoadLibrary, e);
73                         }
74                 }
75                 return library;
76         }
77
78         private final String socketFile;
79
80         private AtomicBoolean connected = new AtomicBoolean();
81
82         private volatile int socketFd = -1;
83
84         /**
85          * Creates a new instance.
86          *
87          * @param socketFile
88          *            to use; if {@code null} or empty, use environment variable
89          *            SSH_AUTH_SOCK
90          */
91         public UnixDomainSocketConnector(String socketFile) {
92                 super();
93                 String file = socketFile;
94                 if (StringUtils.isEmptyOrNull(file)) {
95                         file = SystemReader.getInstance().getenv(ENV_SSH_AUTH_SOCK);
96                 }
97                 this.socketFile = file;
98         }
99
100         @Override
101         public boolean connect() throws IOException {
102                 if (StringUtils.isEmptyOrNull(socketFile)) {
103                         return false;
104                 }
105                 int fd = socketFd;
106                 synchronized (this) {
107                         if (connected.get()) {
108                                 return true;
109                         }
110                         UnixSockets sockets = getLibrary();
111                         if (sockets == null) {
112                                 return false;
113                         }
114                         try {
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());
123                                 connected.set(true);
124                         } catch (LastErrorException e) {
125                                 if (fd >= 0) {
126                                         try {
127                                                 sockets.close(fd);
128                                         } catch (LastErrorException e1) {
129                                                 e.addSuppressed(e1);
130                                         }
131                                 }
132                                 throw new IOException(MessageFormat
133                                                 .format(Texts.get().msgConnectFailed, socketFile), e);
134                         }
135                 }
136                 socketFd = fd;
137                 return connected.get();
138         }
139
140         @Override
141         public synchronized void close() throws IOException {
142                 int fd = socketFd;
143                 if (connected.getAndSet(false) && fd >= 0) {
144                         socketFd = -1;
145                         try {
146                                 getLibrary().close(fd);
147                         } catch (LastErrorException e) {
148                                 throw new IOException(MessageFormat.format(
149                                                 Texts.get().msgCloseFailed, Integer.toString(fd)), e);
150                         }
151                 }
152         }
153
154         @Override
155         public byte[] rpc(byte command, byte[] message) throws IOException {
156                 prepareMessage(command, message);
157                 int fd = socketFd;
158                 if (!connected.get() || fd < 0) {
159                         // No translation, internal error
160                         throw new IllegalStateException("Not connected to SSH agent"); //$NON-NLS-1$
161                 }
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);
169                 return payload;
170         }
171
172         private void writeFully(int fd, byte[] message) throws IOException {
173                 int toWrite = message.length;
174                 try {
175                         byte[] buf = message;
176                         while (toWrite > 0) {
177                                 int written = getLibrary()
178                                                 .write(fd, buf, new LibCAPI.size_t(buf.length))
179                                                 .intValue();
180                                 if (written < 0) {
181                                         throw new IOException(
182                                                         MessageFormat.format(Texts.get().msgSendFailed,
183                                                                         Integer.toString(message.length),
184                                                                         Integer.toString(toWrite)));
185                                 }
186                                 toWrite -= written;
187                                 if (written > 0 && toWrite > 0) {
188                                         buf = Arrays.copyOfRange(buf, written, buf.length);
189                                 }
190                         }
191                 } catch (LastErrorException e) {
192                         throw new IOException(
193                                         MessageFormat.format(Texts.get().msgSendFailed,
194                                                         Integer.toString(message.length),
195                                                         Integer.toString(toWrite)),
196                                         e);
197                 }
198         }
199
200         private void readFully(int fd, byte[] data) throws IOException {
201                 int n = 0;
202                 int offset = 0;
203                 while (offset < data.length
204                                 && (n = read(fd, data, offset, data.length - offset)) > 0) {
205                         offset += n;
206                 }
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)));
212                 }
213         }
214
215         private int read(int fd, byte[] buffer, int offset, int length)
216                         throws IOException {
217                 try {
218                         LibCAPI.size_t toRead = new LibCAPI.size_t(length);
219                         if (offset == 0) {
220                                 return getLibrary().read(fd, buffer, toRead).intValue();
221                         }
222                         byte[] data = new byte[length];
223                         int read = getLibrary().read(fd, data, toRead).intValue();
224                         if (read > 0) {
225                                 System.arraycopy(data, 0, buffer, offset, read);
226                         }
227                         return read;
228                 } catch (LastErrorException e) {
229                         throw new IOException(
230                                         MessageFormat.format(Texts.get().msgReadFailed,
231                                                         Integer.toString(length)),
232                                         e);
233                 }
234         }
235 }