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