1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.common.util.channel.impl;
17
18 import java.io.BufferedOutputStream;
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.util.ArrayList;
26 import java.util.List;
27
28 import org.apache.commons.io.FileUtils;
29 import org.apache.commons.io.FilenameUtils;
30 import org.apache.commons.io.IOUtils;
31 import org.apache.commons.lang3.StringUtils;
32 import org.kuali.common.util.Assert;
33 import org.kuali.common.util.CollectionUtils;
34 import org.kuali.common.util.FormatUtils;
35 import org.kuali.common.util.LocationUtils;
36 import org.kuali.common.util.PropertyUtils;
37 import org.kuali.common.util.Str;
38 import org.kuali.common.util.base.Threads;
39 import org.kuali.common.util.channel.api.SecureChannel;
40 import org.kuali.common.util.channel.model.ChannelContext;
41 import org.kuali.common.util.channel.model.CommandContext;
42 import org.kuali.common.util.channel.model.CommandResult;
43 import org.kuali.common.util.channel.model.CopyDirection;
44 import org.kuali.common.util.channel.model.CopyResult;
45 import org.kuali.common.util.channel.model.RemoteFile;
46 import org.kuali.common.util.channel.model.Status;
47 import org.kuali.common.util.channel.util.ChannelUtils;
48 import org.kuali.common.util.channel.util.SSHUtils;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import com.google.common.base.Optional;
53 import com.google.common.collect.ImmutableList;
54 import com.jcraft.jsch.Channel;
55 import com.jcraft.jsch.ChannelExec;
56 import com.jcraft.jsch.ChannelSftp;
57 import com.jcraft.jsch.JSch;
58 import com.jcraft.jsch.JSchException;
59 import com.jcraft.jsch.Session;
60 import com.jcraft.jsch.SftpATTRS;
61 import com.jcraft.jsch.SftpException;
62
63 public final class DefaultSecureChannel implements SecureChannel {
64
65 private static final Logger logger = LoggerFactory.getLogger(DefaultSecureChannel.class);
66
67 private static final String SFTP = "sftp";
68 private static final String EXEC = "exec";
69 private static final String FORWARDSLASH = "/";
70
71 private final Session session;
72 private final ChannelSftp sftp;
73 private final ChannelContext context;
74
75 private boolean closed = false;
76
77 public DefaultSecureChannel(ChannelContext context) throws IOException {
78 Assert.noNulls(context);
79 this.context = context;
80 log();
81 try {
82 JSch jsch = getJSch();
83 this.session = openSession(jsch);
84 this.sftp = openSftpChannel(session, context.getConnectTimeout());
85 } catch (JSchException e) {
86 throw new IOException("Unexpected error opening secure channel", e);
87 }
88 }
89
90 @Override
91 public synchronized void close() {
92 if (closed) {
93 return;
94 }
95 if (context.isEcho()) {
96 logger.info("Closing secure channel [{}]", ChannelUtils.getLocation(context.getUsername(), context.getHostname()));
97 } else {
98 logger.debug("Closing secure channel [{}]", ChannelUtils.getLocation(context.getUsername(), context.getHostname()));
99 }
100 closeQuietly(sftp);
101 closeQuietly(session);
102 this.closed = true;
103 }
104
105 @Override
106 public List<CommandResult> exec(String... commands) {
107 List<CommandResult> results = new ArrayList<CommandResult>();
108 List<String> copy = ImmutableList.copyOf(commands);
109 for (String command : copy) {
110 CommandResult result = exec(command);
111 results.add(result);
112 }
113 return results;
114 }
115
116 @Override
117 public List<CommandResult> exec(CommandContext... contexts) {
118 List<CommandResult> results = new ArrayList<CommandResult>();
119 List<CommandContext> copy = ImmutableList.copyOf(contexts);
120 for (CommandContext context : copy) {
121 CommandResult result = exec(context);
122 results.add(result);
123 }
124 return results;
125 }
126
127 @Override
128 public CommandResult exec(String command) {
129 return exec(new CommandContext.Builder(command).build());
130 }
131
132 @Override
133 public CommandResult exec(CommandContext context) {
134 StreamHandler handler = new StreamHandler(context);
135 ChannelExec exec = null;
136 try {
137
138 long start = System.currentTimeMillis();
139
140 exec = getChannelExec();
141
142 exec.setCommand(context.getCommand());
143
144 exec.setInputStream(context.getStdin().orNull());
145
146 handler.openStreams(exec, this.context.getEncoding());
147
148 handler.startPumping();
149
150 connect(exec, context.getTimeout());
151
152 waitForClosed(exec, this.context.getWaitForClosedSleepMillis());
153
154 handler.waitUntilDone();
155
156 handler.validate();
157
158 CommandResult result = new CommandResult(context.getCommand(), exec.getExitStatus(), start);
159
160 validate(context, result);
161
162 if (this.context.isEcho()) {
163 String elapsed = FormatUtils.getTime(result.getElapsed());
164 logger.info("{} - [{}]", new String(context.getCommand(), this.context.getEncoding()), elapsed);
165 }
166
167 return result;
168 } catch (Exception e) {
169
170 handler.disableQuietly();
171 throw new IllegalStateException(e);
172 } finally {
173
174 IOUtils.closeQuietly(context.getStdin().orNull());
175 closeQuietly(exec);
176 handler.closeQuietly();
177 }
178 }
179
180 protected void validate(CommandContext context, CommandResult result) {
181 if (context.isIgnoreExitValue()) {
182 return;
183 }
184 if (context.getSuccessCodes().size() == 0) {
185 return;
186 }
187 List<Integer> codes = context.getSuccessCodes();
188 int exitValue = result.getExitValue();
189 for (int successCode : codes) {
190 if (exitValue == successCode) {
191 return;
192 }
193 }
194 throw new IllegalStateException("Command exited with [" + exitValue + "]. Valid values are [" + CollectionUtils.toCSV(codes) + "]");
195 }
196
197 protected ChannelExec getChannelExec() throws JSchException {
198 ChannelExec exec = (ChannelExec) session.openChannel(EXEC);
199 if (context.isRequestPseudoTerminal()) {
200 exec.setPty(true);
201 }
202 return exec;
203 }
204
205 @Override
206 public void execNoWait(String command) {
207 execNoWait(Str.getBytes(command, context.getEncoding()));
208 }
209
210 @Override
211 public void execNoWait(byte[] command) {
212 Assert.noNulls(command);
213 ChannelExec exec = null;
214 try {
215 if (context.isEcho()) {
216 logger.info("{}", Str.getString(command, context.getEncoding()));
217 }
218
219 exec = getChannelExec();
220
221 exec.setCommand(command);
222
223
224 connect(exec, Optional.<Integer> absent());
225 } catch (Exception e) {
226 throw new IllegalStateException(e);
227 } finally {
228 closeQuietly(exec);
229 }
230 }
231
232 protected void waitForClosed(ChannelExec exec, long millis) {
233 while (!exec.isClosed()) {
234 Threads.sleep(millis);
235 }
236 }
237
238 @Override
239 public RemoteFile getWorkingDirectory() {
240 try {
241 String workingDirectory = sftp.pwd();
242 return getMetaData(workingDirectory);
243 } catch (SftpException e) {
244 throw new IllegalStateException(e);
245 }
246 }
247
248 protected void log() {
249 if (context.isEcho()) {
250 logger.info("Opening secure channel [{}] encoding={}", ChannelUtils.getLocation(context.getUsername(), context.getHostname()), context.getEncoding());
251 } else {
252 logger.debug("Opening secure channel [{}] encoding={}", ChannelUtils.getLocation(context.getUsername(), context.getHostname()), context.getEncoding());
253 }
254 logger.debug("Private key files - {}", context.getPrivateKeyFiles().size());
255 logger.debug("Private key strings - {}", context.getPrivateKeys().size());
256 logger.debug("Private key config file - {}", context.getConfig());
257 logger.debug("Private key config file use - {}", context.isUseConfigFile());
258 logger.debug("Include default private key locations - {}", context.isIncludeDefaultPrivateKeyLocations());
259 logger.debug("Known hosts file - {}", context.getKnownHosts());
260 logger.debug("Port - {}", context.getPort());
261 if (context.getConnectTimeout().isPresent()) {
262 logger.debug("Connect timeout - {}", context.getConnectTimeout().get());
263 }
264 logger.debug("Strict host key checking - {}", context.isStrictHostKeyChecking());
265 logger.debug("Configuring channel with {} custom options", context.getOptions().size());
266 PropertyUtils.debug(context.getOptions());
267 }
268
269 protected ChannelSftp openSftpChannel(Session session, Optional<Integer> timeout) throws JSchException {
270 ChannelSftp sftp = (ChannelSftp) session.openChannel(SFTP);
271 connect(sftp, timeout);
272 return sftp;
273 }
274
275 protected void connect(Channel channel, Optional<Integer> timeout) throws JSchException {
276 if (timeout.isPresent()) {
277 channel.connect(timeout.get());
278 } else {
279 channel.connect();
280 }
281 }
282
283 protected void closeQuietly(Session session) {
284 if (session != null) {
285 session.disconnect();
286 }
287 }
288
289 protected void closeQuietly(Channel channel) {
290 if (channel != null) {
291 channel.disconnect();
292 }
293 }
294
295 protected Session openSession(JSch jsch) throws JSchException {
296 Session session = jsch.getSession(context.getUsername().orNull(), context.getHostname(), context.getPort());
297
298 session.setConfig(context.getOptions());
299 if (context.getConnectTimeout().isPresent()) {
300 session.connect(context.getConnectTimeout().get());
301 } else {
302 session.connect();
303 }
304 return session;
305 }
306
307 protected JSch getJSch() {
308 try {
309 JSch jsch = getJSch(context.getPrivateKeyFiles(), context.getPrivateKeys());
310 File knownHosts = context.getKnownHosts();
311 if (context.isUseKnownHosts() && knownHosts.exists()) {
312 String path = LocationUtils.getCanonicalPath(knownHosts);
313 jsch.setKnownHosts(path);
314 }
315 return jsch;
316 } catch (JSchException e) {
317 throw new IllegalStateException("Unexpected error", e);
318 }
319 }
320
321 protected JSch getJSch(List<File> privateKeys, List<String> privateKeyStrings) throws JSchException {
322 JSch jsch = new JSch();
323 for (File privateKey : privateKeys) {
324 String path = LocationUtils.getCanonicalPath(privateKey);
325 jsch.addIdentity(path);
326 }
327 int count = 0;
328 for (String privateKeyString : privateKeyStrings) {
329 String name = "privateKeyString-" + Integer.toString(count++);
330 byte[] bytes = Str.getBytes(privateKeyString, context.getEncoding());
331 jsch.addIdentity(name, bytes, null, null);
332 }
333 return jsch;
334 }
335
336 protected static List<File> getUniquePrivateKeyFiles(List<File> privateKeys, boolean useConfigFile, File config, boolean includeDefaultPrivateKeyLocations) {
337 List<String> paths = new ArrayList<String>();
338 for (File privateKey : privateKeys) {
339 paths.add(LocationUtils.getCanonicalPath(privateKey));
340 }
341 if (useConfigFile) {
342 for (String path : SSHUtils.getFilenames(config)) {
343 paths.add(path);
344 }
345 }
346 if (includeDefaultPrivateKeyLocations) {
347 for (String path : SSHUtils.PRIVATE_KEY_DEFAULTS) {
348 paths.add(path);
349 }
350 }
351 List<String> uniquePaths = CollectionUtils.getUniqueStrings(paths);
352 return SSHUtils.getExistingAndReadable(uniquePaths);
353 }
354
355 @Override
356 public RemoteFile getMetaData(String absolutePath) {
357 Assert.noBlanks(absolutePath);
358 return fillInAttributes(absolutePath);
359 }
360
361 @Override
362 public void deleteFile(String absolutePath) {
363 RemoteFile file = getMetaData(absolutePath);
364 if (isStatus(file, Status.MISSING)) {
365 return;
366 }
367 if (file.isDirectory()) {
368 throw new IllegalArgumentException("[" + ChannelUtils.getLocation(context.getUsername(), context.getHostname(), file) + "] is a directory.");
369 }
370 try {
371 sftp.rm(absolutePath);
372 if (context.isEcho()) {
373 logger.info("deleted -> [{}]", absolutePath);
374 }
375 } catch (SftpException e) {
376 throw new IllegalStateException(e);
377 }
378 }
379
380 @Override
381 public boolean exists(String absolutePath) {
382 RemoteFile file = getMetaData(absolutePath);
383 return isStatus(file, Status.EXISTS);
384 }
385
386 @Override
387 public boolean isDirectory(String absolutePath) {
388 RemoteFile file = getMetaData(absolutePath);
389 return isStatus(file, Status.EXISTS) && file.isDirectory();
390 }
391
392 protected RemoteFile fillInAttributes(String path) {
393 try {
394 SftpATTRS attributes = sftp.stat(path);
395 return fillInAttributes(path, attributes);
396 } catch (SftpException e) {
397 return handleNoSuchFileException(path, e);
398 }
399 }
400
401 protected RemoteFile fillInAttributes(String path, SftpATTRS attributes) {
402 boolean directory = attributes.isDir();
403 int permissions = attributes.getPermissions();
404 int userId = attributes.getUId();
405 int groupId = attributes.getGId();
406 long size = attributes.getSize();
407 Status status = Status.EXISTS;
408 return new RemoteFile.Builder(path).directory(directory).permissions(permissions).userId(userId).groupId(groupId).size(size).status(status).build();
409 }
410
411 @Override
412 public CopyResult scp(File source, RemoteFile destination) {
413 Assert.notNull(source);
414 Assert.exists(source);
415 Assert.isFalse(source.isDirectory(), "[" + source + "] is a directory");
416 Assert.isTrue(source.canRead(), "[" + source + "] not readable");
417 return scp(LocationUtils.getCanonicalURLString(source), destination);
418 }
419
420 @Override
421 public CopyResult scpToDir(File source, RemoteFile directory) {
422 String filename = source.getName();
423 String absolutePath = getAbsolutePath(directory.getAbsolutePath(), filename);
424 RemoteFile file = new RemoteFile.Builder(absolutePath).clone(directory).build();
425 return scp(source, file);
426 }
427
428 @Override
429 public CopyResult scp(String location, RemoteFile destination) {
430 Assert.notNull(location);
431 Assert.isTrue(LocationUtils.exists(location), location + " does not exist");
432 InputStream in = null;
433 try {
434 in = LocationUtils.getInputStream(location);
435 return scp(in, destination);
436 } catch (Exception e) {
437 throw new IllegalStateException(e);
438 } finally {
439 IOUtils.closeQuietly(in);
440 }
441 }
442
443 @Override
444 public CopyResult scpString(String string, RemoteFile destination) {
445 Assert.notNull(string);
446 InputStream in = new ByteArrayInputStream(Str.getBytes(string, context.getEncoding()));
447 CopyResult result = scp(in, destination);
448 IOUtils.closeQuietly(in);
449 return result;
450 }
451
452 @Override
453 public String toString(RemoteFile source) {
454 Assert.notNull(source);
455 ByteArrayOutputStream out = new ByteArrayOutputStream();
456 try {
457 scp(source, out);
458 return out.toString(context.getEncoding());
459 } catch (IOException e) {
460 throw new IllegalStateException("Unexpected IO error", e);
461 } finally {
462 IOUtils.closeQuietly(out);
463 }
464 }
465
466 @Override
467 public CopyResult scp(InputStream source, RemoteFile destination) {
468 Assert.notNull(source);
469 try {
470 long start = System.currentTimeMillis();
471 createDirectories(destination);
472 sftp.put(source, destination.getAbsolutePath());
473 RemoteFile meta = getMetaData(destination.getAbsolutePath());
474 CopyResult result = new CopyResult(start, meta.getSize().get(), CopyDirection.TO_REMOTE);
475 to(destination, result);
476 return result;
477 } catch (SftpException e) {
478 throw new IllegalStateException(e);
479 }
480 }
481
482 protected String getAbsolutePath(String absolutePath, String filename) {
483 if (StringUtils.endsWith(absolutePath, FORWARDSLASH)) {
484 return absolutePath + filename;
485 } else {
486 return absolutePath + FORWARDSLASH + filename;
487 }
488 }
489
490 @Override
491 public CopyResult scpToDir(String location, RemoteFile directory) {
492 String filename = LocationUtils.getFilename(location);
493 String absolutePath = getAbsolutePath(directory.getAbsolutePath(), filename);
494 RemoteFile file = new RemoteFile.Builder(absolutePath).clone(directory).build();
495 return scp(location, file);
496 }
497
498 @Override
499 public CopyResult scp(RemoteFile source, File destination) {
500 OutputStream out = null;
501 try {
502 out = new BufferedOutputStream(FileUtils.openOutputStream(destination));
503 return scp(source, out);
504 } catch (Exception e) {
505 throw new IllegalStateException(e);
506 } finally {
507 IOUtils.closeQuietly(out);
508 }
509 }
510
511 @Override
512 public CopyResult scp(String absolutePath, OutputStream out) throws IOException {
513 try {
514 long start = System.currentTimeMillis();
515 sftp.get(absolutePath, out);
516 RemoteFile meta = getMetaData(absolutePath);
517 CopyResult result = new CopyResult(start, meta.getSize().get(), CopyDirection.FROM_REMOTE);
518 from(absolutePath, result);
519 return result;
520 } catch (SftpException e) {
521 throw new IOException("Unexpected IO error", e);
522 }
523 }
524
525
526
527
528 protected void to(RemoteFile destination, CopyResult result) {
529 if (context.isEcho()) {
530 String elapsed = FormatUtils.getTime(result.getElapsedMillis());
531 String rate = FormatUtils.getRate(result.getElapsedMillis(), result.getAmountInBytes());
532 Object[] args = { destination.getAbsolutePath(), elapsed, rate };
533 logger.info("created -> [{}] - [{}, {}]", args);
534 }
535 }
536
537
538
539
540 protected void from(String absolutePath, CopyResult result) {
541 if (context.isEcho()) {
542 String elapsed = FormatUtils.getTime(result.getElapsedMillis());
543 String rate = FormatUtils.getRate(result.getElapsedMillis(), result.getAmountInBytes());
544 Object[] args = { absolutePath, elapsed, rate };
545 logger.info("copied <- [{}] - [{}, {}]", args);
546 }
547 }
548
549 @Override
550 public CopyResult scp(RemoteFile source, OutputStream out) throws IOException {
551 return scp(source.getAbsolutePath(), out);
552 }
553
554 @Override
555 public CopyResult scpToDir(RemoteFile source, File destination) {
556 String filename = FilenameUtils.getName(source.getAbsolutePath());
557 File newDestination = new File(destination, filename);
558 return scp(source, newDestination);
559 }
560
561 @Override
562 public void createDirectory(RemoteFile dir) {
563 Assert.isTrue(dir.isDirectory());
564 try {
565 createDirectories(dir);
566 if (context.isEcho()) {
567 logger.info("mkdir -> [{}]", dir.getAbsolutePath());
568 }
569 } catch (SftpException e) {
570 throw new IllegalStateException(e);
571 }
572 }
573
574 protected void createDirectories(RemoteFile file) throws SftpException {
575 boolean directoryIndicator = file.isDirectory();
576 RemoteFile remoteFile = fillInAttributes(file.getAbsolutePath());
577 validate(remoteFile, directoryIndicator);
578 List<String> directories = LocationUtils.getNormalizedPathFragments(file.getAbsolutePath(), file.isDirectory());
579 for (String directory : directories) {
580 RemoteFile parentDir = fillInAttributes(directory);
581 validate(parentDir, true);
582 if (!isStatus(parentDir, Status.EXISTS)) {
583 mkdir(parentDir);
584 }
585 }
586 }
587
588 protected boolean isStatus(RemoteFile file, Status status) {
589 Optional<Status> remoteStatus = file.getStatus();
590 if (remoteStatus.isPresent()) {
591 return remoteStatus.get().equals(status);
592 } else {
593 return false;
594 }
595 }
596
597 protected void validate(RemoteFile file, boolean directoryIndicator) {
598
599 Assert.isTrue(file.getStatus().isPresent());
600
601
602 boolean missing = isStatus(file, Status.MISSING);
603 boolean exists = isStatus(file, Status.EXISTS);
604
605
606
607 boolean correctFileType = file.isDirectory() == directoryIndicator;
608
609
610 boolean valid = missing || exists && correctFileType;
611
612 Assert.isTrue(valid, getInvalidExistingFileMessage(file));
613 }
614
615 protected String getInvalidExistingFileMessage(RemoteFile existing) {
616 if (existing.isDirectory()) {
617 return "[" + ChannelUtils.getLocation(context.getUsername(), context.getHostname(), existing) + "] is an existing directory. Unable to create file.";
618 } else {
619 return "[" + ChannelUtils.getLocation(context.getUsername(), context.getHostname(), existing) + "] is an existing file. Unable to create directory.";
620 }
621 }
622
623 protected void mkdir(RemoteFile dir) {
624 try {
625 String path = dir.getAbsolutePath();
626 logger.debug("Creating [{}]", path);
627 sftp.mkdir(path);
628 setAttributes(dir);
629 } catch (SftpException e) {
630 throw new IllegalStateException(e);
631 }
632 }
633
634 protected void setAttributes(RemoteFile file) throws SftpException {
635 String path = file.getAbsolutePath();
636 if (file.getPermissions().isPresent()) {
637 sftp.chmod(file.getPermissions().get(), path);
638 }
639 if (file.getGroupId().isPresent()) {
640 sftp.chgrp(file.getGroupId().get(), path);
641 }
642 if (file.getUserId().isPresent()) {
643 sftp.chown(file.getUserId().get(), path);
644 }
645 }
646
647 protected RemoteFile handleNoSuchFileException(String path, SftpException e) {
648 if (isNoSuchFileException(e)) {
649 return new RemoteFile.Builder(path).status(Status.MISSING).build();
650 } else {
651 throw new IllegalStateException(e);
652 }
653 }
654
655 protected boolean isNoSuchFileException(SftpException exception) {
656 return exception.id == ChannelSftp.SSH_FX_NO_SUCH_FILE;
657 }
658
659 }