1 /*
2 * SSHTools - Java SSH2 API
3 *
4 * Copyright (C) 2002-2003 Lee David Painter and Contributors.
5 *
6 * Contributions made by:
7 *
8 * Brett Smith
9 * Richard Pernavas
10 * Erwin Bolwidt
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 */
26 package com.sshtools.j2ssh;
27
28 import java.io.BufferedInputStream;
29 import java.io.EOFException;
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import com.sshtools.j2ssh.configuration.ConfigurationLoader;
36 import com.sshtools.j2ssh.connection.ChannelEventListener;
37 import com.sshtools.j2ssh.session.SessionChannelClient;
38
39 /**
40 * <p>
41 * Implements a Secure Copy (SCP) client. This may be useful when the server
42 * does not support SFTP.
43 * </p>
44 *
45 * @author Lee David Painter
46 * @version $Revision: 1.18 $
47 *
48 * @since 0.2.0
49 */
50 public final class ScpClient {
51 private SshClient ssh;
52 private File cwd;
53 private boolean verbose;
54 private ChannelEventListener eventListener;
55 /**
56 * <p>
57 * Creates an SCP client. CWD (Current working directory) will be the CWD
58 * of the process that started this JVM.
59 * </p>
60 *
61 * @param ssh A connected SshClient
62 * @param verbose Output verbose detail
63 * @param eventListener
64 *
65 * @since 0.2.0
66 */
67 public ScpClient(SshClient ssh, boolean verbose,
68 ChannelEventListener eventListener) {
69 this(new File(ConfigurationLoader.checkAndGetProperty("user.dir", ".")),
70 ssh, verbose, eventListener);
71 }
72
73 /**
74 * <p>
75 * Creates an SCP client.
76 * </p>
77 *
78 * @param cwd The current local directory
79 * @param ssh A connected SshClient
80 * @param verbose Output verbose detail
81 * @param eventListener
82 *
83 * @since 0.2.0
84 */
85 public ScpClient(File cwd, SshClient ssh, boolean verbose,
86 ChannelEventListener eventListener) {
87 this.ssh = ssh;
88 this.cwd = cwd;
89 this.verbose = verbose;
90 this.eventListener = eventListener;
91 }
92
93 /**
94 * <p>
95 * Uploads a <code>java.io.InputStream</code> to a remove server as file.
96 * You <strong>must</strong> supply the correct number of bytes that will
97 * be written.
98 * </p>
99 *
100 * @param in stream providing file
101 * @param length number of bytes that will be written
102 * @param localFile local file name
103 * @param remoteFile remote file name
104 *
105 * @throws IOException on any error
106 */
107 public void put(InputStream in, long length, String localFile,
108 String remoteFile) throws IOException {
109 ScpChannel scp = new ScpChannel("scp -t " + (verbose ? "-v " : "")
110 + remoteFile);
111 scp.addEventListener(eventListener);
112 if (!ssh.openChannel(scp)) {
113 throw new IOException("Failed to open SCP channel");
114 }
115 scp.waitForResponse();
116 scp.writeStreamToRemote(in, length, localFile);
117 scp.close();
118 }
119
120 /**
121 * <p>
122 * Gets a remote file as an <code>java.io.InputStream</code>.
123 * </p>
124 *
125 * @param remoteFile remote file name
126 *
127 * @return stream
128 *
129 * @throws IOException on any error
130 */
131 public InputStream get(String remoteFile) throws IOException {
132 ScpChannel scp = new ScpChannel("scp " + "-f " + (verbose ? "-v " : "")
133 + remoteFile);
134 scp.addEventListener(eventListener);
135 if (!ssh.openChannel(scp)) {
136 throw new IOException("Failed to open SCP Channel");
137 }
138 return scp.readStreamFromRemote();
139 }
140
141 /**
142 * <p>
143 * Uploads a local file onto the remote server.
144 * </p>
145 *
146 * @param localFile The path to the local file relative to the local
147 * current directory; may be a file or directory
148 * @param remoteFile The path on the remote server, may be a file or
149 * directory
150 * @param recursive Copy the contents of a directory recursivly
151 *
152 * @throws IOException if an IO error occurs during the operation
153 *
154 * @since 0.2.0
155 */
156 public void put(String localFile, String remoteFile, boolean recursive) throws
157 IOException {
158 File lf = new File(localFile);
159 if (!lf.isAbsolute()) {
160 lf = new File(cwd, localFile);
161 }
162 if (!lf.exists()) {
163 throw new IOException(localFile + " does not exist");
164 }
165 if (!lf.isFile() && !lf.isDirectory()) {
166 throw new IOException(localFile
167 + " is not a regular file or directory");
168 }
169 if (lf.isDirectory() && !recursive) {
170 throw new IOException(localFile
171 + " is a directory, use recursive mode");
172 }
173 if ( (remoteFile == null) || remoteFile.equals("")) {
174 remoteFile = ".";
175 }
176 ScpChannel scp = new ScpChannel("scp "
177 + (lf.isDirectory() ? "-d " : "") + "-t "
178 + (recursive ? "-r " : "") +
179 (verbose ? "-v " : "")
180 + remoteFile);
181 scp.addEventListener(eventListener);
182 if (!ssh.openChannel(scp)) {
183 throw new IOException("Failed to open SCP channel");
184 }
185 scp.waitForResponse();
186 scp.writeFileToRemote(lf, recursive);
187 scp.close();
188 }
189
190 /**
191 * <p>
192 * Uploads an array of local files onto the remote server.
193 * </p>
194 *
195 * @param localFiles an array of local files; may be files or directories
196 * @param remoteFile the path on the remote server, may be a file or
197 * directory1
198 * @param recursive Copy the contents of directorys recursivly
199 *
200 * @throws IOException if an IO error occurs during the operation
201 *
202 * @since 0.2.0
203 */
204 public void put(String[] localFiles, String remoteFile, boolean recursive) throws
205 IOException {
206 if ( (remoteFile == null) || remoteFile.equals("")) {
207 remoteFile = ".";
208 }
209 if (localFiles.length == 1) {
210 put(localFiles[0], remoteFile, recursive);
211 }
212 else {
213 ScpChannel scp = new ScpChannel("scp " + "-d -t "
214 + (recursive ? "-r " : "") +
215 (verbose ? "-v " : "")
216 + remoteFile);
217 scp.addEventListener(eventListener);
218 if (!ssh.openChannel(scp)) {
219 throw new IOException("Failed to open SCP channel");
220 }
221 scp.waitForResponse();
222 for (int i = 0; i < localFiles.length; i++) {
223 File lf = new File(localFiles[i]);
224 if (!lf.isAbsolute()) {
225 lf = new File(cwd, localFiles[i]);
226 }
227 if (!lf.isFile() && !lf.isDirectory()) {
228 throw new IOException(lf.getName()
229 + " is not a regular file or directory");
230 }
231 scp.writeFileToRemote(lf, recursive);
232 }
233 scp.close();
234 }
235 }
236
237 /**
238 * <p>
239 * Downloads an array of remote files to the local computer.
240 * </p>
241 *
242 * @param localFile The local path to place the files
243 * @param remoteFiles The path of the remote files
244 * @param recursive recursivly copy the contents of a directory
245 *
246 * @throws IOException if an IO error occurs during the operation
247 *
248 * @since 0.2.0
249 */
250 public void get(String localFile, String[] remoteFiles, boolean recursive) throws
251 IOException {
252 StringBuffer buf = new StringBuffer();
253 for (int i = 0; i < remoteFiles.length; i++) {
254 buf.append("\"");
255 buf.append(remoteFiles[i]);
256 buf.append("\" ");
257 }
258 String remoteFile = buf.toString();
259 remoteFile = remoteFile.trim();
260 get(localFile, remoteFile, recursive);
261 }
262
263 /**
264 * <p>
265 * Downloads a remote file onto the local computer.
266 * </p>
267 *
268 * @param localFile The path to place the file
269 * @param remoteFile The path of the file on the remote server
270 * @param recursive recursivly copy the contents of a directory
271 *
272 * @throws IOException if an IO error occurs during the operation
273 *
274 * @since 0.2.0
275 */
276 public void get(String localFile, String remoteFile, boolean recursive) throws
277 IOException {
278 if ( (localFile == null) || localFile.equals("")) {
279 localFile = ".";
280 }
281 File lf = new File(localFile);
282 if (!lf.isAbsolute()) {
283 lf = new File(cwd, localFile);
284 }
285 if (lf.exists() && !lf.isFile() && !lf.isDirectory()) {
286 throw new IOException(localFile
287 + " is not a regular file or directory");
288 }
289 ScpChannel scp = new ScpChannel("scp " + "-f "
290 + (recursive ? "-r " : "") +
291 (verbose ? "-v " : "")
292 + remoteFile);
293 scp.addEventListener(eventListener);
294 if (!ssh.openChannel(scp)) {
295 throw new IOException("Failed to open SCP Channel");
296 }
297 scp.readFromRemote(lf);
298 scp.close();
299 }
300
301 /**
302 * <p>
303 * Implements an SCP channel by extending the
304 * <code>SessionChannelClient</code>.
305 * </p>
306 *
307 * @since 0.2.0
308 */
309 class ScpChannel
310 extends SessionChannelClient {
311 byte[] buffer = new byte[16384];
312 String cmd;
313 /**
314 * <p>
315 * Contruct the channel with the specified scp command.
316 * </p>
317 *
318 * @param cmd The scp command
319 *
320 * @since 0.2.0
321 */
322 ScpChannel(String cmd) {
323 this.cmd = cmd;
324 setName("scp");
325 }
326
327 /**
328 * <p>
329 * This implementation executes the scp command when the channel is
330 * opened.
331 * </p>
332 *
333 * @throws IOException
334 *
335 * @since 0.2.0
336 */
337 protected void onChannelOpen() throws IOException {
338 if (!executeCommand(cmd)) {
339 throw new IOException("Failed to execute the command " + cmd);
340 }
341 }
342
343 /**
344 * <p>
345 * Writes a directory to the remote server.
346 * </p>
347 *
348 * @param dir The source directory
349 * @param recursive Add the contents of the directory recursivley
350 *
351 * @return true if the file was written, otherwise false
352 *
353 * @throws IOException if an IO error occurs
354 *
355 * @since 0.2.0
356 */
357 private boolean writeDirToRemote(File dir, boolean recursive) throws
358 IOException {
359 if (!recursive) {
360 writeError("File " + dir.getName()
361 + " is a directory, use recursive mode");
362 return false;
363 }
364 String cmd = "D0755 0 " + dir.getName() + "\n";
365 out.write(cmd.getBytes());
366 waitForResponse();
367 String[] list = dir.list();
368 for (int i = 0; i < list.length; i++) {
369 File f = new File(dir, list[i]);
370 writeFileToRemote(f, recursive);
371 }
372 out.write("E\n".getBytes());
373 return true;
374 }
375
376 /**
377 * <p>
378 * Write a stream as a file to the remote server. You
379 * <strong>must</strong> supply the correct number of bytes that will
380 * be written.
381 * </p>
382 *
383 * @param in stream
384 * @param length number of bytes to write
385 * @param localName local file name
386 *
387 * @throws IOException if an IO error occurs
388 *
389 * @since 0.2.0
390 */
391 private void writeStreamToRemote(InputStream in, long length,
392 String localName) throws IOException {
393 String cmd = "C0644 " + length + " " + localName + "\n";
394 out.write(cmd.getBytes());
395 waitForResponse();
396 writeCompleteFile(in, length);
397 writeOk();
398 waitForResponse();
399 }
400
401 /**
402 * <p>
403 * Write a file to the remote server.
404 * </p>
405 *
406 * @param file The source file
407 * @param recursive Add the contents of the directory recursivley
408 *
409 * @throws IOException if an IO error occurs
410 *
411 * @since 0.2.0
412 */
413 private void writeFileToRemote(File file, boolean recursive) throws
414 IOException {
415 if (file.isDirectory()) {
416 if (!writeDirToRemote(file, recursive)) {
417 return;
418 }
419 }
420 else if (file.isFile()) {
421 String cmd = "C0644 " + file.length() + " " + file.getName()
422 + "\n";
423 out.write(cmd.getBytes());
424 waitForResponse();
425 FileInputStream fi = new FileInputStream(file);
426 writeCompleteFile(fi, file.length());
427 writeOk();
428 }
429 else {
430 throw new IOException(file.getName() + " not valid for SCP");
431 }
432 waitForResponse();
433 }
434
435 private void readFromRemote(File file) throws IOException {
436 String cmd;
437 String[] cmdParts = new String[3];
438 writeOk();
439 while (true) {
440 try {
441 cmd = readString();
442 }
443 catch (EOFException e) {
444 return;
445 }
446 char cmdChar = cmd.charAt(0);
447 switch (cmdChar) {
448 case 'E':
449 writeOk();
450 return;
451 case 'T':
452 throw new IOException("SCP time not supported: " + cmd);
453 case 'C':
454 case 'D':
455 String targetName = file.getAbsolutePath();
456 parseCommand(cmd, cmdParts);
457 if (file.isDirectory()) {
458 targetName += (File.separator + cmdParts[2]);
459 }
460 File targetFile = new File(targetName);
461 if (cmdChar == 'D') {
462 if (targetFile.exists()) {
463 if (!targetFile.isDirectory()) {
464 String msg = "Invalid target "
465 + targetFile.getName()
466 + ", must be a directory";
467 writeError(msg);
468 throw new IOException(msg);
469 }
470 }
471 else {
472 if (!targetFile.mkdir()) {
473 String msg = "Could not create directory: "
474 + targetFile.getName();
475 writeError(msg);
476 throw new IOException(msg);
477 }
478 }
479 readFromRemote(targetFile);
480 continue;
481 }
482 FileOutputStream fo = new FileOutputStream(targetFile);
483 writeOk();
484 long len = Long.parseLong(cmdParts[1]);
485 readCompleteFile(fo, len);
486 waitForResponse();
487 writeOk();
488 break;
489 default:
490 writeError("Unexpected cmd: " + cmd);
491 throw new IOException("SCP unexpected cmd: " + cmd);
492 }
493 }
494 }
495
496 private InputStream readStreamFromRemote() throws IOException {
497 String cmd;
498 String[] cmdParts = new String[3];
499 writeOk();
500 try {
501 cmd = readString();
502 }
503 catch (EOFException e) {
504 return null;
505 }
506 char cmdChar = cmd.charAt(0);
507 switch (cmdChar) {
508 case 'E':
509 writeOk();
510 return null;
511 case 'T':
512 throw new IOException("SCP time not supported: " + cmd);
513 case 'D':
514 throw new IOException(
515 "Directories cannot be copied to a stream");
516 case 'C':
517 parseCommand(cmd, cmdParts);
518 writeOk();
519 long len = Long.parseLong(cmdParts[1]);
520 return new BufferedInputStream(new ScpInputStream(len, in, this),
521 16 * 1024);
522 default:
523 writeError("Unexpected cmd: " + cmd);
524 throw new IOException("SCP unexpected cmd: " + cmd);
525 }
526 }
527
528 private void parseCommand(String cmd, String[] cmdParts) throws IOException {
529 int l;
530 int r;
531 l = cmd.indexOf(' ');
532 r = cmd.indexOf(' ', l + 1);
533 if ( (l == -1) || (r == -1)) {
534 writeError("Syntax error in cmd");
535 throw new IOException("Syntax error in cmd");
536 }
537 cmdParts[0] = cmd.substring(1, l);
538 cmdParts[1] = cmd.substring(l + 1, r);
539 cmdParts[2] = cmd.substring(r + 1);
540 }
541
542 private String readString() throws IOException {
543 int ch;
544 int i = 0;
545 while ( ( (ch = in.read()) != ( (int) '\n')) && (ch >= 0)) {
546 buffer[i++] = (byte) ch;
547 }
548 if (ch == -1) {
549 throw new EOFException("SCP returned unexpected EOF");
550 }
551 if (buffer[0] == (byte) '\n') {
552 throw new IOException("Unexpected <NL>");
553 }
554 if ( (buffer[0] == (byte) '\02') || (buffer[0] == (byte) '\01')) {
555 String msg = new String(buffer, 1, i - 1);
556 if (buffer[0] == (byte) '\02') {
557 throw new IOException(msg);
558 }
559 throw new IOException("SCP returned an unexpected error: "
560 + msg);
561 }
562 return new String(buffer, 0, i);
563 }
564
565 private void waitForResponse() throws IOException {
566 int r = in.read();
567 if (r == 0) {
568 // All is well, no error
569 return;
570 }
571 if (r == -1) {
572 throw new EOFException("SCP returned unexpected EOF");
573 }
574 String msg = readString();
575 if (r == (byte) '\02') {
576 throw new IOException(msg);
577 }
578 throw new IOException("SCP returned an unexpected error: " + msg);
579 }
580
581 private void writeOk() throws IOException {
582 out.write(0);
583 }
584
585 private void writeError(String reason) throws IOException {
586 out.write(1);
587 out.write(reason.getBytes());
588 }
589
590 private void writeCompleteFile(InputStream file, long size) throws
591 IOException {
592 int count = 0;
593 int read;
594 try {
595 while (count < size) {
596 read = file.read(buffer, 0,
597 (int) ( ( (size - count) < buffer.length)
598 ? (size - count) : buffer.length));
599 if (read == -1) {
600 throw new EOFException("SCP received an unexpected EOF");
601 }
602 count += read;
603 out.write(buffer, 0, read);
604 }
605 }
606 finally {
607 file.close();
608 }
609 }
610
611 private void readCompleteFile(FileOutputStream file, long size) throws
612 IOException {
613 int count = 0;
614 int read;
615 try {
616 while (count < size) {
617 read = in.read(buffer, 0,
618 (int) ( ( (size - count) < buffer.length)
619 ? (size - count) : buffer.length));
620 if (read == -1) {
621 throw new EOFException("SCP received an unexpected EOF");
622 }
623 count += read;
624 file.write(buffer, 0, read);
625 }
626 }
627 finally {
628 file.close();
629 }
630 }
631 }
632
633 class ScpInputStream
634 extends InputStream {
635 long length;
636 InputStream in;
637 long count;
638 ScpChannel channel;
639 ScpInputStream(long length, InputStream in, ScpChannel channel) {
640 this.length = length;
641 this.in = in;
642 this.channel = channel;
643 }
644
645 public int read() throws IOException {
646 if (count == length) {
647 return -1;
648 }
649 if (count >= length) {
650 throw new EOFException("End of file.");
651 }
652 int r = in.read();
653 if (r == -1) {
654 throw new EOFException("Unexpected EOF.");
655 }
656 count++;
657 if (count == length) {
658 channel.waitForResponse();
659 channel.writeOk();
660 }
661 return r;
662 }
663
664 public void close() throws IOException {
665 channel.close();
666 }
667 }
668 }