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.daemon.scp;
27
28 import com.sshtools.daemon.platform;
29 import com.sshtools.daemon.util;
30
31 import com.sshtools.j2ssh;
32 import com.sshtools.j2ssh.io;
33 import com.sshtools.j2ssh.sftp;
34
35 import org.apache.commons.logging;
36
37 import java.io;
38
39 import java.util;
40
41
42 /**
43 *
44 *
45 * @author $author$
46 * @version $Revision: 1.8 $
47 */
48 public class ScpServer extends NativeProcessProvider implements Runnable {
49 private static Log log = LogFactory.getLog(ScpServer.class);
50 private static int BUFFER_SIZE = 16384;
51
52 // Private instance variables
53 private InputStream in;
54
55 // Private instance variables
56 private InputStream err;
57 private OutputStream out;
58 private String destination;
59 private PipedOutputStream pipeIn;
60 private PipedOutputStream pipeErr;
61 private PipedInputStream pipeOut;
62 private SshThread scpServerThread;
63 private int verbosity = 0;
64 private int exitCode;
65 private boolean directory;
66 private boolean recursive;
67 private boolean from;
68 private boolean to;
69 private NativeFileSystemProvider nfs;
70 private byte[] buffer = new byte[BUFFER_SIZE];
71 private String currentDirectory;
72 private boolean preserveAttributes;
73
74 /**
75 * Creates a new ScpServer object.
76 */
77 public ScpServer() {
78 nfs = NativeFileSystemProvider.getInstance();
79 }
80
81 /* (non-Javadoc)
82 * @see com.sshtools.daemon.platform.NativeProcessProvider#allocatePseudoTerminal(java.lang.String, int, int, int, int, java.lang.String)
83 */
84 public boolean allocatePseudoTerminal(String term, int cols, int rows,
85 int width, int height, String modes) {
86 return false;
87 }
88
89 /* (non-Javadoc)
90 * @see com.sshtools.daemon.platform.NativeProcessProvider#createProcess(java.lang.String, java.util.Map)
91 */
92 public boolean createProcess(String command, Map environment)
93 throws IOException {
94 log.info("Creating ScpServer");
95
96 if (nfs == null) {
97 throw new IOException(
98 "NativeFileSystem was not instantiated. Please check logs");
99 }
100
101 scp(command.substring(4));
102
103 return true;
104 }
105
106 /* (non-Javadoc)
107 * @see com.sshtools.daemon.platform.NativeProcessProvider#getDefaultTerminalProvider()
108 */
109 public String getDefaultTerminalProvider() {
110 return null;
111 }
112
113 /* (non-Javadoc)
114 * @see com.sshtools.daemon.platform.NativeProcessProvider#getInputStream()
115 */
116 public InputStream getInputStream() throws IOException {
117 return in;
118 }
119
120 /* (non-Javadoc)
121 * @see com.sshtools.daemon.platform.NativeProcessProvider#getStderrInputStream()
122 */
123 public InputStream getStderrInputStream() {
124 return err;
125 }
126
127 /* (non-Javadoc)
128 * @see com.sshtools.daemon.platform.NativeProcessProvider#getOutputStream()
129 */
130 public OutputStream getOutputStream() throws IOException {
131 return out;
132 }
133
134 /* (non-Javadoc)
135 * @see com.sshtools.daemon.platform.NativeProcessProvider#kill()
136 */
137 public void kill() {
138 log.info("Killing ScpServer");
139
140 try {
141 if (pipeIn != null) {
142 pipeIn.close();
143 }
144 } catch (IOException ioe) {
145 }
146
147 try {
148 if (pipeOut != null) {
149 pipeOut.close();
150 }
151 } catch (IOException ioe) {
152 }
153
154 try {
155 if (pipeErr != null) {
156 pipeErr.close();
157 }
158 } catch (IOException ioe) {
159 }
160 }
161
162 /* (non-Javadoc)
163 * @see com.sshtools.daemon.platform.NativeProcessProvider#start()
164 */
165 public void start() throws IOException {
166 log.debug("Starting ScpServer thread");
167 scpServerThread = SshThread.getCurrentThread().cloneThread(this,
168 "ScpServer");
169 scpServerThread.start();
170 }
171
172 /* (non-Javadoc)
173 * @see com.sshtools.daemon.platform.NativeProcessProvider#stillActive()
174 */
175 public boolean stillActive() {
176 return false;
177 }
178
179 /* (non-Javadoc)
180 * @see com.sshtools.daemon.platform.NativeProcessProvider#supportsPseudoTerminal(java.lang.String)
181 */
182 public boolean supportsPseudoTerminal(String term) {
183 return false;
184 }
185
186 /* (non-Javadoc)
187 * @see com.sshtools.daemon.platform.NativeProcessProvider#waitForExitCode()
188 */
189 public int waitForExitCode() {
190 try {
191 synchronized (this) {
192 wait();
193 }
194 } catch (InterruptedException ie) {
195 }
196
197 log.debug("Returning exit code of " + exitCode);
198
199 return exitCode;
200 }
201
202 private void scp(String args) throws IOException {
203 log.debug("Parsing ScpServer options " + args);
204
205 // Parse the command line for supported options
206 String[] a = StringUtil.current().allParts(args, " ");
207 destination = null;
208 directory = false;
209 from = false;
210 to = false;
211 recursive = false;
212 verbosity = 0;
213
214 boolean remote = false;
215
216 for (int i = 0; i < a.length; i++) {
217 if (a[i].startsWith("-")) {
218 String s = a[i].substring(1);
219
220 for (int j = 0; j < s.length(); j++) {
221 char ch = s.charAt(j);
222
223 switch (ch) {
224 case 't':
225 to = true;
226
227 break;
228
229 case 'd':
230 directory = true;
231
232 break;
233
234 case 'f':
235 from = true;
236
237 break;
238
239 case 'r':
240 recursive = true;
241
242 break;
243
244 case 'v':
245 verbosity++;
246
247 break;
248
249 case 'p':
250 preserveAttributes = true;
251
252 break;
253
254 default:
255 log.warn("Unsupported argument, allowing to continue.");
256 }
257 }
258 } else {
259 if (destination == null) {
260 destination = a[i];
261 } else {
262 throw new IOException("More than one destination supplied " +
263 a[i]);
264 }
265 }
266 }
267
268 if (!to && !from) {
269 throw new IOException("Must supply either -t or -f.");
270 }
271
272 if (destination == null) {
273 throw new IOException("Destination not supplied.");
274 }
275
276 log.debug("Destination is " + destination);
277 log.debug("Recursive is " + recursive);
278 log.debug("Directory is " + directory);
279 log.debug("Verbosity is " + verbosity);
280 log.debug("From is " + from);
281 log.debug("To is " + to);
282 log.debug("Preserve Attributes " + preserveAttributes);
283
284 // Start the SCP server
285 log.debug("Creating pipes");
286 pipeIn = new PipedOutputStream();
287 pipeErr = new PipedOutputStream();
288 pipeOut = new PipedInputStream();
289 in = new PipedInputStream(pipeIn);
290 err = new PipedInputStream(pipeErr);
291 out = new PipedOutputStream(pipeOut);
292 }
293
294 /**
295 * Send ok command to client
296 *
297 * @throws IOException on any error
298 */
299 private void writeOk() throws IOException {
300 log.debug("Sending client ok command");
301 pipeIn.write(0);
302 pipeIn.flush();
303 }
304
305 /**
306 * Send command to client
307 *
308 * @param cmd command
309 *
310 * @throws IOException on any error
311 */
312 private void writeCommand(String cmd) throws IOException {
313 log.debug("Sending command '" + cmd + "'");
314 pipeIn.write(cmd.getBytes());
315
316 if (!cmd.endsWith("\n")) {
317 pipeIn.write("\n".getBytes());
318 }
319
320 pipeIn.flush();
321 }
322
323 /**
324 * Send error message to client
325 *
326 * @param msg error message
327 *
328 * @throws IOException on any error
329 */
330 private void writeError(String msg) throws IOException {
331 writeError(msg, false);
332 }
333
334 /**
335 * Send error message to client
336 *
337 * @param msg error message
338 * @param serious serious error
339 *
340 * @throws IOException on any error
341 */
342 private void writeError(String msg, boolean serious)
343 throws IOException {
344 log.debug("Sending error message '" + msg + "' to client (serious=" +
345 serious + ")");
346 pipeIn.write(serious ? 2 : 1);
347 pipeIn.write(msg.getBytes());
348
349 if (!msg.endsWith("\n")) {
350 pipeIn.write('\n');
351 }
352
353 pipeIn.flush();
354 }
355
356 /* (non-Javadoc)
357 * @see java.lang.Runnable#run()
358 */
359 public void run() {
360 log.debug("Running ScpServer thread");
361
362 try {
363 if (from) {
364 log.info("From mode");
365
366 try {
367 waitForResponse();
368
369 // Build a string pattern that may be used to match wildcards
370 StringPattern sp = new StringPattern(destination);
371
372 /*If this looks like a wildcard, then attempt a simple expansion.
373 * This only work for the base part of the file name at the moment
374 */
375 if (sp.hasWildcard()) {
376 log.debug("Path contains wildcard");
377
378 String base = destination;
379 String dir = ".";
380 int idx = base.lastIndexOf('/');
381
382 if (idx != -1) {
383 if (idx > 0) {
384 dir = base.substring(0, idx);
385 }
386
387 base = base.substring(idx + 1);
388 }
389
390 log.debug("Looking for matches in " + dir + " for " +
391 base);
392 sp = new StringPattern(base);
393
394 byte[] handle = null;
395
396 try {
397 handle = nfs.openDirectory(dir);
398
399 SftpFile[] files = nfs.readDirectory(handle);
400
401 for (int i = 0; i < files.length; i++) {
402 log.debug("Testing for match against " +
403 files[i].getFilename());
404
405 if (sp.matches(files[i].getFilename())) {
406 log.debug("Matched");
407 writeFileToRemote(dir + "/" +
408 files[i].getFilename());
409 } else {
410 log.debug("No match");
411 }
412 }
413 } finally {
414 if (handle != null) {
415 try {
416 nfs.closeFile(handle);
417 } catch (Exception e) {
418 }
419 }
420 }
421 } else {
422 log.debug("No wildcards");
423 writeFileToRemote(destination);
424 }
425
426 log.debug("File transfers complete");
427 } catch (FileNotFoundException fnfe) {
428 log.error(fnfe);
429 writeError(fnfe.getMessage(), true);
430 throw new IOException(fnfe.getMessage());
431 } catch (PermissionDeniedException pde) {
432 log.error(pde);
433 writeError(pde.getMessage(), true);
434 throw new IOException(pde.getMessage());
435 } catch (InvalidHandleException ihe) {
436 log.error(ihe);
437 writeError(ihe.getMessage(), true);
438 throw new IOException(ihe.getMessage());
439 } catch (IOException ioe) {
440 log.error(ioe);
441 writeError(ioe.getMessage(), true);
442 throw new IOException(ioe.getMessage());
443 }
444 } else {
445 log.info("To mode");
446 readFromRemote(destination);
447 }
448 } catch (Throwable t) {
449 t.printStackTrace();
450 log.error(t);
451 exitCode = 1;
452 }
453
454 //
455 log.debug("ScpServer stopped, notify block on waitForExitCode().");
456
457 synchronized (this) {
458 notify();
459 }
460 }
461
462 private boolean writeDirToRemote(String path) throws IOException {
463 FileAttributes attr = nfs.getFileAttributes(path);
464
465 if (attr.isDirectory() && !recursive) {
466 writeError("File " + path + " is a directory, use recursive mode");
467
468 return false;
469 }
470
471 String basename = path;
472 int idx = path.lastIndexOf('/');
473
474 if (idx != -1) {
475 basename = path.substring(idx + 1);
476 }
477
478 writeCommand("D" + attr.getMaskString() + " 0 " + basename + "\n");
479 waitForResponse();
480
481 byte[] handle = null;
482
483 try {
484 handle = nfs.openDirectory(path);
485
486 SftpFile[] list = nfs.readDirectory(handle);
487
488 for (int i = 0; i < list.length; i++) {
489 writeFileToRemote(path + "/" + list[i].getFilename());
490 }
491
492 writeCommand("E");
493 } catch (InvalidHandleException ihe) {
494 throw new IOException(ihe.getMessage());
495 } catch (PermissionDeniedException e) {
496 throw new IOException(e.getMessage());
497 } finally {
498 if (handle != null) {
499 try {
500 nfs.closeFile(handle);
501 } catch (Exception e) {
502 log.error(e);
503 }
504 }
505 }
506
507 return true;
508 }
509
510 private void writeFileToRemote(String path)
511 throws IOException, PermissionDeniedException, InvalidHandleException {
512 FileAttributes attr = nfs.getFileAttributes(path);
513
514 if (attr.isDirectory()) {
515 if (!writeDirToRemote(path)) {
516 return;
517 }
518 } else if (attr.isFile()) {
519 String basename = path;
520 int idx = basename.lastIndexOf('/');
521
522 if (idx != -1) {
523 basename = path.substring(idx + 1);
524 }
525
526 // TODO: Deal with permissions properly
527 writeCommand("C" + attr.getMaskString() + " " + attr.getSize() +
528 " " + basename + "\n");
529 waitForResponse();
530 log.debug("Opening file " + path);
531
532 byte[] handle = null;
533
534 try {
535 handle = nfs.openFile(path,
536 new UnsignedInteger32(
537 NativeFileSystemProvider.OPEN_READ), attr);
538
539 int count = 0;
540 log.debug("Sending file");
541
542 while (count < attr.getSize().intValue()) {
543 try {
544 byte[] buf = nfs.readFile(handle,
545 new UnsignedInteger64(String.valueOf(count)),
546 new UnsignedInteger32(BUFFER_SIZE));
547 count += buf.length;
548 log.debug("Writing block of " + buf.length + " bytes");
549 pipeIn.write(buf);
550 } catch (EOFException eofe) {
551 log.debug("End of file - finishing transfer");
552
553 break;
554 }
555 }
556
557 pipeIn.flush();
558
559 if (count < attr.getSize().intValue()) {
560 throw new IOException(
561 "File transfer terminated abnormally.");
562 } else {
563 log.info("File transfer complete.");
564 }
565
566 writeOk();
567 } finally {
568 if (handle != null) {
569 try {
570 nfs.closeFile(handle);
571 } catch (Exception e) {
572 log.error(e);
573 }
574 }
575 }
576 } else {
577 throw new IOException(path + " not valid for SCP.");
578 }
579
580 waitForResponse();
581 }
582
583 private void waitForResponse() throws IOException {
584 log.debug("Waiting for response");
585
586 int r = pipeOut.read();
587
588 if (r == 0) {
589 log.debug("Got Ok");
590
591 // All is well, no error
592 return;
593 }
594
595 if (r == -1) {
596 throw new EOFException("SCP returned unexpected EOF");
597 }
598
599 String msg = readString();
600 log.debug("Got error '" + msg + "'");
601
602 if (r == (byte) '\02') {
603 log.debug("This is a serious error");
604 throw new IOException(msg);
605 }
606
607 throw new IOException("SCP returned an unexpected error: " + msg);
608 }
609
610 private void readFromRemote(String path) throws IOException {
611 String cmd;
612 String[] cmdParts = new String[3];
613 writeOk();
614
615 while (true) {
616 log.debug("Waiting for command");
617
618 try {
619 cmd = readString();
620 } catch (EOFException e) {
621 return;
622 }
623
624 log.debug("Got command '" + cmd + "'");
625
626 char cmdChar = cmd.charAt(0);
627
628 switch (cmdChar) {
629 case 'E':
630 writeOk();
631
632 return;
633
634 case 'T':
635 log.error("SCP time not currently supported");
636 writeError(
637 "WARNING: This server does not currently support the SCP time command");
638
639 break;
640
641 case 'C':
642 case 'D':
643 parseCommand(cmd, cmdParts);
644
645 FileAttributes attr = null;
646
647 try {
648 log.debug("Getting attributes for current destination (" +
649 path + ")");
650 attr = nfs.getFileAttributes(path);
651 } catch (FileNotFoundException fnfe) {
652 log.debug("Current destination not found");
653 }
654
655 String targetPath = path;
656 String name = cmdParts[2];
657
658 if ((attr != null) && attr.isDirectory()) {
659 log.debug("Target is a directory");
660 targetPath += ('/' + name);
661 }
662
663 FileAttributes targetAttr = null;
664
665 try {
666 log.debug("Getting attributes for target destination (" +
667 targetPath + ")");
668 targetAttr = nfs.getFileAttributes(targetPath);
669 } catch (FileNotFoundException fnfe) {
670 log.debug("Target destination not found");
671 }
672
673 if (cmdChar == 'D') {
674 log.debug("Got directory request");
675
676 if (targetAttr != null) {
677 if (!targetAttr.isDirectory()) {
678 String msg = "Invalid target " + name +
679 ", must be a directory";
680 writeError(msg);
681 throw new IOException(msg);
682 }
683 } else {
684 try {
685 log.debug("Creating directory " + targetPath);
686
687 if (!nfs.makeDirectory(targetPath)) {
688 String msg = "Could not create directory: " +
689 name;
690 writeError(msg);
691 throw new IOException(msg);
692 } else {
693 log.debug("Setting permissions on directory");
694 attr.setPermissionsFromMaskString(cmdParts[0]);
695 }
696 } catch (FileNotFoundException e1) {
697 writeError("File not found");
698 throw new IOException("File not found");
699 } catch (PermissionDeniedException e1) {
700 writeError("Permission denied");
701 throw new IOException("Permission denied");
702 }
703 }
704
705 readFromRemote(targetPath);
706
707 continue;
708 }
709
710 log.debug("Opening file for writing");
711
712 byte[] handle = null;
713
714 try {
715 // Open the file
716 handle = nfs.openFile(targetPath,
717 new UnsignedInteger32(NativeFileSystemProvider.OPEN_CREATE |
718 NativeFileSystemProvider.OPEN_WRITE |
719 NativeFileSystemProvider.OPEN_TRUNCATE), attr);
720 log.debug("NFS file opened");
721 writeOk();
722 log.debug("Reading from client");
723
724 int count = 0;
725 int read;
726 long length = Long.parseLong(cmdParts[1]);
727
728 while (count < length) {
729 read = pipeOut.read(buffer, 0,
730 (int) (((length - count) < buffer.length)
731 ? (length - count) : buffer.length));
732
733 if (read == -1) {
734 throw new EOFException(
735 "ScpServer received an unexpected EOF during file transfer");
736 }
737
738 log.debug("Got block of " + read);
739 nfs.writeFile(handle,
740 new UnsignedInteger64(String.valueOf(count)),
741 buffer, 0, read);
742 count += read;
743 }
744
745 log.debug("File transfer complete");
746 } catch (InvalidHandleException ihe) {
747 writeError("Invalid handle.");
748 throw new IOException("Invalid handle.");
749 } catch (FileNotFoundException e) {
750 writeError("File not found");
751 throw new IOException("File not found");
752 } catch (PermissionDeniedException e) {
753 writeError("Permission denied");
754 throw new IOException("Permission denied");
755 } finally {
756 if (handle != null) {
757 try {
758 log.debug("Closing handle");
759 nfs.closeFile(handle);
760 } catch (Exception e) {
761 }
762 }
763 }
764
765 waitForResponse();
766
767 if (preserveAttributes) {
768 attr.setPermissionsFromMaskString(cmdParts[0]);
769 log.debug("Setting permissions on directory to " +
770 attr.getPermissionsString());
771
772 try {
773 nfs.setFileAttributes(targetPath, attr);
774 } catch (Exception e) {
775 writeError("Failed to set file permissions.");
776
777 break;
778 }
779 }
780
781 writeOk();
782
783 break;
784
785 default:
786 writeError("Unexpected cmd: " + cmd);
787 throw new IOException("SCP unexpected cmd: " + cmd);
788 }
789 }
790 }
791
792 private void parseCommand(String cmd, String[] cmdParts)
793 throws IOException {
794 int l;
795 int r;
796 l = cmd.indexOf(' ');
797 r = cmd.indexOf(' ', l + 1);
798
799 if ((l == -1) || (r == -1)) {
800 writeError("Syntax error in cmd");
801 throw new IOException("Syntax error in cmd");
802 }
803
804 cmdParts[0] = cmd.substring(1, l);
805 cmdParts[1] = cmd.substring(l + 1, r);
806 cmdParts[2] = cmd.substring(r + 1);
807 }
808
809 private String readString() throws IOException {
810 int ch;
811 int i = 0;
812
813 while (((ch = pipeOut.read()) != ((int) '\n')) && (ch >= 0)) {
814 buffer[i++] = (byte) ch;
815 }
816
817 if (ch == -1) {
818 throw new EOFException("SCP returned unexpected EOF");
819 }
820
821 if (buffer[0] == (byte) '\n') {
822 throw new IOException("Unexpected <NL>");
823 }
824
825 if ((buffer[0] == (byte) '\02') || (buffer[0] == (byte) '\01')) {
826 String msg = new String(buffer, 1, i - 1);
827
828 if (buffer[0] == (byte) '\02') {
829 throw new IOException(msg);
830 }
831
832 throw new IOException("SCP returned an unexpected error: " + msg);
833 }
834
835 return new String(buffer, 0, i);
836 }
837 }