1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.vfs2.provider.sftp;
18
19 import java.io.IOException;
20 import java.io.InputStreamReader;
21 import java.nio.charset.StandardCharsets;
22 import java.time.Duration;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Objects;
26
27 import org.apache.commons.lang3.time.DurationUtils;
28 import org.apache.commons.logging.Log;
29 import org.apache.commons.logging.LogFactory;
30 import org.apache.commons.vfs2.Capability;
31 import org.apache.commons.vfs2.FileObject;
32 import org.apache.commons.vfs2.FileSystemException;
33 import org.apache.commons.vfs2.FileSystemOptions;
34 import org.apache.commons.vfs2.provider.AbstractFileName;
35 import org.apache.commons.vfs2.provider.AbstractFileSystem;
36 import org.apache.commons.vfs2.provider.GenericFileName;
37
38 import com.jcraft.jsch.ChannelExec;
39 import com.jcraft.jsch.ChannelSftp;
40 import com.jcraft.jsch.JSchException;
41 import com.jcraft.jsch.Session;
42 import com.jcraft.jsch.SftpException;
43
44
45
46
47 public class SftpFileSystem extends AbstractFileSystem {
48
49 private static final Log LOG = LogFactory.getLog(SftpFileSystem.class);
50
51 private static final int UNIDENTIFIED = -1;
52
53 private static final int SLEEP_MILLIS = 100;
54
55 private static final int EXEC_BUFFER_SIZE = 128;
56
57 private static final long LAST_MOD_TIME_ACCURACY = 1000L;
58
59
60
61
62
63
64
65 private volatile Session session;
66
67 private volatile ChannelSftp idleChannel;
68
69 private final Duration connectTimeout;
70
71
72
73
74
75
76
77 private volatile int uid = UNIDENTIFIED;
78
79
80
81
82
83
84
85 private volatile int[] groupsIds;
86
87
88
89
90 private final boolean execDisabled;
91
92
93
94
95
96
97
98
99 protected SftpFileSystem(final GenericFileName rootName, final Session session, final FileSystemOptions fileSystemOptions) {
100 super(rootName, null, fileSystemOptions);
101 this.session = Objects.requireNonNull(session, "session");
102 connectTimeout = SftpFileSystemConfigBuilder.getInstance().getConnectTimeout(fileSystemOptions);
103 if (SftpFileSystemConfigBuilder.getInstance().isDisableDetectExecChannel(fileSystemOptions)) {
104 execDisabled = true;
105 } else {
106 execDisabled = detectExecDisabled();
107 }
108 }
109
110
111
112
113 @Override
114 protected void addCapabilities(final Collection<Capability> caps) {
115 caps.addAll(SftpFileProvider.capabilities);
116 }
117
118
119
120
121 @Override
122 protected FileObject createFile(final AbstractFileName name) throws FileSystemException {
123 return new SftpFileObject(name, this);
124 }
125
126
127
128
129
130
131 private boolean detectExecDisabled() {
132 try {
133 return getUId() == UNIDENTIFIED;
134 } catch (final JSchException | IOException e) {
135 LOG.debug("Cannot get UID, assuming no exec channel is present", e);
136 return true;
137 }
138 }
139
140 @Override
141 protected void doCloseCommunicationLink() {
142 if (idleChannel != null) {
143 synchronized (this) {
144 if (idleChannel != null) {
145 idleChannel.disconnect();
146 idleChannel = null;
147 }
148 }
149 }
150
151 if (session != null) {
152 session.disconnect();
153 }
154 }
155
156
157
158
159
160
161
162
163
164
165
166 private int executeCommand(final String command, final StringBuilder output) throws JSchException, IOException {
167 final ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
168 try {
169 channel.setCommand(command);
170 channel.setInputStream(null);
171 try (InputStreamReader stream = new InputStreamReader(channel.getInputStream(), StandardCharsets.UTF_8)) {
172 channel.setErrStream(System.err, true);
173 channel.connect(DurationUtils.toMillisInt(connectTimeout));
174
175
176 final char[] buffer = new char[EXEC_BUFFER_SIZE];
177 int read;
178 while ((read = stream.read(buffer, 0, buffer.length)) >= 0) {
179 output.append(buffer, 0, read);
180 }
181 }
182
183
184 while (!channel.isClosed()) {
185 try {
186 Thread.sleep(SLEEP_MILLIS);
187 } catch (final InterruptedException e) {
188
189 break;
190 }
191 }
192 } finally {
193 channel.disconnect();
194 }
195 return channel.getExitStatus();
196 }
197
198
199
200
201
202
203
204
205 protected ChannelSftp getChannel() throws IOException {
206 try {
207
208 ChannelSftp channel = null;
209 if (idleChannel != null) {
210 synchronized (this) {
211 if (idleChannel != null) {
212 channel = idleChannel;
213 idleChannel = null;
214 }
215 }
216 }
217
218 if (channel == null) {
219 channel = (ChannelSftp) getSession().openChannel("sftp");
220 channel.connect(DurationUtils.toMillisInt(connectTimeout));
221 final Boolean userDirIsRoot = SftpFileSystemConfigBuilder.getInstance()
222 .getUserDirIsRoot(getFileSystemOptions());
223 final String workingDirectory = getRootName().getPath();
224 if (workingDirectory != null && (userDirIsRoot == null || !userDirIsRoot.booleanValue())) {
225 try {
226 channel.cd(workingDirectory);
227 } catch (final SftpException e) {
228 throw new FileSystemException("vfs.provider.sftp/change-work-directory.error", workingDirectory,
229 e);
230 }
231 }
232 }
233
234 final String fileNameEncoding = SftpFileSystemConfigBuilder.getInstance()
235 .getFileNameEncoding(getFileSystemOptions());
236
237 if (fileNameEncoding != null) {
238 try {
239 channel.setFilenameEncoding(fileNameEncoding);
240 } catch (final SftpException e) {
241 throw new FileSystemException("vfs.provider.sftp/filename-encoding.error", fileNameEncoding);
242 }
243 }
244 return channel;
245 } catch (final JSchException e) {
246 throw new FileSystemException("vfs.provider.sftp/connect.error", getRootName().getFriendlyURI(), e);
247 }
248 }
249
250
251
252
253
254
255
256
257
258 public int[] getGroupsIds() throws JSchException, IOException {
259 if (groupsIds == null) {
260 synchronized (this) {
261
262 if (groupsIds == null) {
263 final StringBuilder output = new StringBuilder();
264 final int code = executeCommand("id -G", output);
265 if (code != 0) {
266 throw new JSchException("Could not get the groups id of the current user (error code: " + code + ")");
267 }
268 groupsIds = parseGroupIdOutput(output);
269 }
270 }
271 }
272 return groupsIds;
273 }
274
275
276
277
278
279
280 @Override
281 public double getLastModTimeAccuracy() {
282 return LAST_MOD_TIME_ACCURACY;
283 }
284
285
286
287
288
289
290 private Session getSession() throws FileSystemException {
291 if (!session.isConnected()) {
292 synchronized (this) {
293 if (!session.isConnected()) {
294 doCloseCommunicationLink();
295 session = SftpFileProvider.createSession((GenericFileName) getRootName(),
296 getFileSystemOptions());
297 }
298 }
299 }
300 return session;
301 }
302
303
304
305
306
307
308
309
310
311 public int getUId() throws JSchException, IOException {
312 if (uid == UNIDENTIFIED) {
313 synchronized (this) {
314 if (uid == UNIDENTIFIED) {
315 final StringBuilder output = new StringBuilder();
316 final int code = executeCommand("id -u", output);
317 if (code != 0) {
318 throw new FileSystemException(
319 "Could not get the user id of the current user (error code: " + code + ")");
320 }
321 final String uidString = output.toString().trim();
322 try {
323 uid = Integer.parseInt(uidString);
324 } catch (final NumberFormatException e) {
325 LOG.debug("Cannot convert UID to integer: '" + uidString + "'", e);
326 }
327 }
328 }
329 }
330 return uid;
331 }
332
333
334
335
336
337
338
339 public boolean isExecDisabled() {
340 return execDisabled;
341 }
342
343
344
345
346
347
348
349 int[] parseGroupIdOutput(final StringBuilder output) {
350
351 final String[] groups = output.toString().trim().split("\\s+");
352
353 return Arrays.stream(groups).map(String::trim).filter(s -> !s.isEmpty()).mapToInt(Integer::parseInt).toArray();
354 }
355
356
357
358
359
360
361 protected void putChannel(final ChannelSftp channelSftp) {
362 if (idleChannel == null) {
363 synchronized (this) {
364 if (idleChannel == null) {
365
366 if (channelSftp.isConnected() && !channelSftp.isClosed()) {
367 idleChannel = channelSftp;
368 }
369 } else {
370 channelSftp.disconnect();
371 }
372 }
373 } else {
374 channelSftp.disconnect();
375 }
376 }
377
378 }