001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018: package org.apache.ivy.plugins.repository.vsftp;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.io.InputStreamReader;
023: import java.io.PrintWriter;
024: import java.io.Reader;
025: import java.text.SimpleDateFormat;
026: import java.util.ArrayList;
027: import java.util.List;
028: import java.util.Locale;
029: import java.util.regex.Pattern;
030:
031: import org.apache.ivy.Ivy;
032: import org.apache.ivy.core.IvyContext;
033: import org.apache.ivy.core.IvyThread;
034: import org.apache.ivy.core.event.IvyEvent;
035: import org.apache.ivy.core.event.IvyListener;
036: import org.apache.ivy.core.event.resolve.EndResolveEvent;
037: import org.apache.ivy.plugins.repository.AbstractRepository;
038: import org.apache.ivy.plugins.repository.BasicResource;
039: import org.apache.ivy.plugins.repository.Resource;
040: import org.apache.ivy.plugins.repository.TransferEvent;
041: import org.apache.ivy.util.Message;
042:
043: /**
044: * Repository using SecureCRT vsftp command line program to access an sftp repository This is
045: * especially useful to leverage the gssapi authentication supported by SecureCRT. In caseswhere
046: * usual sftp is enough, prefer the 100% java solution of sftp repository. This requires SecureCRT
047: * to be in the PATH. Tested with SecureCRT 5.0.5
048: */
049: public class VsftpRepository extends AbstractRepository {
050: private static final String PROMPT = "vsftp> ";
051:
052: private static final SimpleDateFormat FORMAT = new SimpleDateFormat(
053: "MMM dd, yyyy HH:mm", Locale.US);
054:
055: private String host;
056:
057: private String username;
058:
059: private String authentication = "gssapi";
060:
061: private Reader in;
062:
063: private Reader err;
064:
065: private PrintWriter out;
066:
067: private volatile StringBuffer errors = new StringBuffer();
068:
069: private long readTimeout = 30000;
070:
071: private long reuseConnection = 5 * 60 * 1000; // reuse connection during 5 minutes by default
072:
073: private volatile long lastCommand;
074:
075: private volatile boolean inCommand;
076:
077: private Process process;
078:
079: private Thread connectionCleaner;
080:
081: private Thread errorsReader;
082:
083: private volatile long errorsLastUpdateTime;
084:
085: private Ivy ivy = null;
086:
087: public Resource getResource(String source) throws IOException {
088: initIvy();
089: return new VsftpResource(this , source);
090: }
091:
092: private void initIvy() {
093: ivy = IvyContext.getContext().getIvy();
094: }
095:
096: protected Resource getInitResource(String source)
097: throws IOException {
098: try {
099: return lslToResource(source, sendCommand("ls -l " + source,
100: true, true));
101: } catch (IOException ex) {
102: cleanup(ex);
103: throw ex;
104: } finally {
105: cleanup();
106: }
107: }
108:
109: public void get(final String source, File destination)
110: throws IOException {
111: initIvy();
112: try {
113: fireTransferInitiated(getResource(source),
114: TransferEvent.REQUEST_GET);
115: File destDir = destination.getParentFile();
116: if (destDir != null) {
117: sendCommand("lcd " + destDir.getAbsolutePath());
118: }
119: if (destination.exists()) {
120: destination.delete();
121: }
122:
123: int index = source.lastIndexOf('/');
124: String srcName = index == -1 ? source : source
125: .substring(index + 1);
126: final File to = destDir == null ? new File(srcName)
127: : new File(destDir, srcName);
128:
129: final IOException[] ex = new IOException[1];
130: Thread get = new IvyThread() {
131: public void run() {
132: initContext();
133: try {
134: sendCommand("get " + source,
135: getExpectedDownloadMessage(source, to),
136: 0);
137: } catch (IOException e) {
138: ex[0] = e;
139: }
140: }
141: };
142: get.start();
143:
144: long prevLength = 0;
145: long lastUpdate = System.currentTimeMillis();
146: long timeout = readTimeout;
147: while (get.isAlive()) {
148: checkInterrupted();
149: long length = to.exists() ? to.length() : 0;
150: if (length > prevLength) {
151: fireTransferProgress(length - prevLength);
152: lastUpdate = System.currentTimeMillis();
153: prevLength = length;
154: } else {
155: if (System.currentTimeMillis() - lastUpdate > timeout) {
156: Message.verbose("download hang for more than "
157: + timeout + "ms. Interrupting.");
158: get.interrupt();
159: if (to.exists()) {
160: to.delete();
161: }
162: throw new IOException(source
163: + " download timeout from " + getHost());
164: }
165: }
166: try {
167: get.join(100);
168: } catch (InterruptedException e) {
169: if (to.exists()) {
170: to.delete();
171: }
172: return;
173: }
174: }
175: if (ex[0] != null) {
176: if (to.exists()) {
177: to.delete();
178: }
179: throw ex[0];
180: }
181:
182: to.renameTo(destination);
183: fireTransferCompleted(destination.length());
184: } catch (IOException ex) {
185: fireTransferError(ex);
186: cleanup(ex);
187: throw ex;
188: } finally {
189: cleanup();
190: }
191: }
192:
193: public List list(String parent) throws IOException {
194: initIvy();
195: try {
196: if (!parent.endsWith("/")) {
197: parent = parent + "/";
198: }
199: String response = sendCommand("ls -l " + parent, true, true);
200: if (response.startsWith("ls")) {
201: return null;
202: }
203: String[] lines = response.split("\n");
204: List ret = new ArrayList(lines.length);
205: for (int i = 0; i < lines.length; i++) {
206: while (lines[i].endsWith("\r")
207: || lines[i].endsWith("\n")) {
208: lines[i] = lines[i].substring(0,
209: lines[i].length() - 1);
210: }
211: if (lines[i].trim().length() != 0) {
212: ret.add(parent
213: + lines[i].substring(lines[i]
214: .lastIndexOf(' ') + 1));
215: }
216: }
217: return ret;
218: } catch (IOException ex) {
219: cleanup(ex);
220: throw ex;
221: } finally {
222: cleanup();
223: }
224: }
225:
226: public void put(File source, String destination, boolean overwrite)
227: throws IOException {
228: initIvy();
229: try {
230: if (getResource(destination).exists()) {
231: if (overwrite) {
232: sendCommand("rm " + destination,
233: getExpectedRemoveMessage(destination));
234: } else {
235: return;
236: }
237: }
238: int index = destination.lastIndexOf('/');
239: String destDir = null;
240: if (index != -1) {
241: destDir = destination.substring(0, index);
242: mkdirs(destDir);
243: sendCommand("cd " + destDir);
244: }
245: String to = destDir != null ? destDir + "/"
246: + source.getName() : source.getName();
247: sendCommand("put " + source.getAbsolutePath(),
248: getExpectedUploadMessage(source, to), 0);
249: sendCommand("mv " + to + " " + destination);
250: } catch (IOException ex) {
251: cleanup(ex);
252: throw ex;
253: } finally {
254: cleanup();
255: }
256: }
257:
258: private void mkdirs(String destDir) throws IOException {
259: if (dirExists(destDir)) {
260: return;
261: }
262: if (destDir.endsWith("/")) {
263: destDir = destDir.substring(0, destDir.length() - 1);
264: }
265: int index = destDir.lastIndexOf('/');
266: if (index != -1) {
267: mkdirs(destDir.substring(0, index));
268: }
269: sendCommand("mkdir " + destDir);
270: }
271:
272: private boolean dirExists(String dir) throws IOException {
273: return !sendCommand("ls " + dir, true).startsWith("ls: ");
274: }
275:
276: protected String sendCommand(String command) throws IOException {
277: return sendCommand(command, false, readTimeout);
278: }
279:
280: protected void sendCommand(String command, Pattern expectedResponse)
281: throws IOException {
282: sendCommand(command, expectedResponse, readTimeout);
283: }
284:
285: /**
286: * The behaviour of vsftp with some commands is to log the resulting message on the error
287: * stream, even if everything is ok. So it's quite difficult if there was an error or not. Hence
288: * we compare the response with the expected message and deal with it. The problem is that this
289: * is very specific to the version of vsftp used for the test, That's why expected messages are
290: * obtained using overridable protected methods.
291: */
292: protected void sendCommand(String command,
293: Pattern expectedResponse, long timeout) throws IOException {
294: String response = sendCommand(command, true, timeout);
295: if (!expectedResponse.matcher(response).matches()) {
296: Message.debug("invalid response from server:");
297: Message.debug("expected: '" + expectedResponse + "'");
298: Message.debug("was: '" + response + "'");
299: throw new IOException(response);
300: }
301: }
302:
303: protected String sendCommand(String command,
304: boolean sendErrorAsResponse) throws IOException {
305: return sendCommand(command, sendErrorAsResponse, readTimeout);
306: }
307:
308: protected String sendCommand(String command,
309: boolean sendErrorAsResponse, boolean single)
310: throws IOException {
311: return sendCommand(command, sendErrorAsResponse, single,
312: readTimeout);
313: }
314:
315: protected String sendCommand(String command,
316: boolean sendErrorAsResponse, long timeout)
317: throws IOException {
318: return sendCommand(command, sendErrorAsResponse, false, timeout);
319: }
320:
321: protected String sendCommand(String command,
322: boolean sendErrorAsResponse, boolean single, long timeout)
323: throws IOException {
324: single = false; // use of alone commands does not work properly due to a long delay between
325: // end of process and end of stream...
326:
327: checkInterrupted();
328: inCommand = true;
329: errorsLastUpdateTime = 0;
330: synchronized (this ) {
331: if (!single || in != null) {
332: ensureConnectionOpened();
333: Message.debug("sending command '" + command + "' to "
334: + getHost());
335: updateLastCommandTime();
336: out.println(command);
337: out.flush();
338: } else {
339: sendSingleCommand(command);
340: }
341: }
342:
343: try {
344: return readResponse(sendErrorAsResponse, timeout);
345: } finally {
346: inCommand = false;
347: if (single) {
348: closeConnection();
349: }
350: }
351: }
352:
353: protected String readResponse(boolean sendErrorAsResponse)
354: throws IOException {
355: return readResponse(sendErrorAsResponse, readTimeout);
356: }
357:
358: protected synchronized String readResponse(
359: final boolean sendErrorAsResponse, long timeout)
360: throws IOException {
361: final StringBuffer response = new StringBuffer();
362: final IOException[] exc = new IOException[1];
363: final boolean[] done = new boolean[1];
364: Runnable r = new Runnable() {
365: public void run() {
366: synchronized (VsftpRepository.this ) {
367: try {
368: int c;
369: boolean getPrompt = false;
370: // the reading is done in a for loop making five attempts to read the stream
371: // if we do not reach the next prompt
372: for (int attempts = 0; !getPrompt
373: && attempts < 5; attempts++) {
374: while ((c = in.read()) != -1) {
375: attempts = 0; // we manage to read something, reset numer of
376: // attempts
377: response.append((char) c);
378: if (response.length() >= PROMPT
379: .length()
380: && response
381: .substring(
382: response
383: .length()
384: - PROMPT
385: .length(),
386: response
387: .length())
388: .equals(PROMPT)) {
389: response.setLength(response
390: .length()
391: - PROMPT.length());
392: getPrompt = true;
393: break;
394: }
395: }
396: if (!getPrompt) {
397: try {
398: Thread.sleep(50);
399: } catch (InterruptedException e) {
400: break;
401: }
402: }
403: }
404: if (getPrompt) {
405: // wait enough for error stream to be fully read
406: if (errorsLastUpdateTime == 0) {
407: // no error written yet, but it may be pending...
408: errorsLastUpdateTime = lastCommand;
409: }
410:
411: while ((System.currentTimeMillis() - errorsLastUpdateTime) < 50) {
412: try {
413: Thread.sleep(30);
414: } catch (InterruptedException e) {
415: break;
416: }
417: }
418: }
419: if (errors.length() > 0) {
420: if (sendErrorAsResponse) {
421: response.append(errors);
422: errors.setLength(0);
423: } else {
424: throw new IOException(chomp(errors)
425: .toString());
426: }
427: }
428: chomp(response);
429: done[0] = true;
430: } catch (IOException e) {
431: exc[0] = e;
432: } finally {
433: VsftpRepository.this .notify();
434: }
435: }
436: }
437: };
438: Thread reader = null;
439: if (timeout == 0) {
440: r.run();
441: } else {
442: reader = new IvyThread(r);
443: reader.start();
444: try {
445: wait(timeout);
446: } catch (InterruptedException e) {
447: //nothing to do
448: }
449: }
450: updateLastCommandTime();
451: if (exc[0] != null) {
452: throw exc[0];
453: } else if (!done[0]) {
454: if (reader != null && reader.isAlive()) {
455: reader.interrupt();
456: for (int i = 0; i < 5 && reader.isAlive(); i++) {
457: try {
458: Thread.sleep(100);
459: } catch (InterruptedException e) {
460: break;
461: }
462: }
463: if (reader.isAlive()) {
464: reader.stop(); // no way to interrupt it non abruptly
465: }
466: }
467: throw new IOException("connection timeout to " + getHost());
468: } else {
469: if ("Not connected.".equals(response)) {
470: Message.info("vsftp connection to " + getHost()
471: + " reset");
472: closeConnection();
473: throw new IOException("not connected to " + getHost());
474: }
475: Message.debug("received response '" + response + "' from "
476: + getHost());
477: return response.toString();
478: }
479: }
480:
481: private synchronized void sendSingleCommand(String command)
482: throws IOException {
483: exec(getSingleCommand(command));
484: }
485:
486: protected synchronized void ensureConnectionOpened()
487: throws IOException {
488: if (in == null) {
489: Message.verbose("connecting to " + getUsername() + "@"
490: + getHost() + "... ");
491: String connectionCommand = getConnectionCommand();
492: exec(connectionCommand);
493:
494: try {
495: readResponse(false); // waits for first prompt
496:
497: if (reuseConnection > 0) {
498: connectionCleaner = new IvyThread() {
499: public void run() {
500: initContext();
501: try {
502: long sleep = 10;
503: while (in != null && sleep > 0) {
504: sleep(sleep);
505: sleep = reuseConnection
506: - (System
507: .currentTimeMillis() - lastCommand);
508: if (inCommand) {
509: sleep = sleep <= 0 ? reuseConnection
510: : sleep;
511: }
512: }
513: } catch (InterruptedException e) {
514: //nothing to do
515: }
516: disconnect();
517: }
518: };
519: connectionCleaner.start();
520: }
521:
522: if (ivy != null) {
523: ivy.getEventManager().addIvyListener(
524: new IvyListener() {
525: public void progress(IvyEvent event) {
526: disconnect();
527: event.getSource()
528: .removeIvyListener(this );
529: }
530: }, EndResolveEvent.NAME);
531: }
532:
533: } catch (IOException ex) {
534: closeConnection();
535: throw new IOException("impossible to connect to "
536: + getUsername() + "@" + getHost() + " using "
537: + getAuthentication() + ": " + ex.getMessage());
538: }
539: Message.verbose("connected to " + getHost());
540: }
541: }
542:
543: private void updateLastCommandTime() {
544: lastCommand = System.currentTimeMillis();
545: }
546:
547: private void exec(String command) throws IOException {
548: Message.debug("launching '" + command + "'");
549: process = Runtime.getRuntime().exec(command);
550: in = new InputStreamReader(process.getInputStream());
551: err = new InputStreamReader(process.getErrorStream());
552: out = new PrintWriter(process.getOutputStream());
553:
554: errorsReader = new IvyThread() {
555: public void run() {
556: initContext();
557: int c;
558: try {
559: while (err != null && (c = err.read()) != -1) {
560: errors.append((char) c);
561: errorsLastUpdateTime = System
562: .currentTimeMillis();
563: }
564: } catch (IOException e) {
565: //nothing to do
566: }
567: }
568: };
569: errorsReader.start();
570: }
571:
572: private void checkInterrupted() {
573: if (ivy != null) {
574: ivy.checkInterrupted();
575: }
576: }
577:
578: /**
579: * Called whenever an api level method end
580: */
581: private void cleanup(Exception ex) {
582: if (ex.getMessage()
583: .equals("connection timeout to " + getHost())) {
584: closeConnection();
585: } else {
586: disconnect();
587: }
588: }
589:
590: /**
591: * Called whenever an api level method end
592: */
593: private void cleanup() {
594: if (reuseConnection == 0) {
595: disconnect();
596: }
597: }
598:
599: public synchronized void disconnect() {
600: if (in != null) {
601: Message.verbose("disconnecting from " + getHost() + "... ");
602: try {
603: sendCommand("exit", false, 300);
604: } catch (IOException e) {
605: //nothing I can do
606: } finally {
607: closeConnection();
608: Message.verbose("disconnected of " + getHost());
609: }
610: }
611: }
612:
613: private synchronized void closeConnection() {
614: if (connectionCleaner != null) {
615: connectionCleaner.interrupt();
616: }
617: if (errorsReader != null) {
618: errorsReader.interrupt();
619: }
620: try {
621: process.destroy();
622: } catch (Exception ex) {
623: //nothing I can do
624: }
625: try {
626: in.close();
627: } catch (Exception e) {
628: //nothing I can do
629: }
630: try {
631: err.close();
632: } catch (Exception e) {
633: //nothing I can do
634: }
635: try {
636: out.close();
637: } catch (Exception e) {
638: //nothing I can do
639: }
640:
641: connectionCleaner = null;
642: errorsReader = null;
643: process = null;
644: in = null;
645: out = null;
646: err = null;
647: Message.debug("connection to " + getHost() + " closed");
648: }
649:
650: /**
651: * Parses a ls -l line and transforms it in a resource
652: *
653: * @param file
654: * @param responseLine
655: * @return
656: */
657: protected Resource lslToResource(String file, String responseLine) {
658: if (responseLine == null || responseLine.startsWith("ls")) {
659: return new BasicResource(file, false, 0, 0, false);
660: } else {
661: String[] parts = responseLine.split("\\s+");
662: if (parts.length != 9) {
663: Message
664: .debug("unrecognized ls format: "
665: + responseLine);
666: return new BasicResource(file, false, 0, 0, false);
667: } else {
668: try {
669: long contentLength = Long.parseLong(parts[3]);
670: String date = parts[4] + " " + parts[5] + " "
671: + parts[6] + " " + parts[7];
672: return new BasicResource(file, true, contentLength,
673: FORMAT.parse(date).getTime(), false);
674: } catch (Exception ex) {
675: Message
676: .warn("impossible to parse server response: "
677: + responseLine + ": " + ex);
678: return new BasicResource(file, false, 0, 0, false);
679: }
680: }
681: }
682: }
683:
684: protected String getSingleCommand(String command) {
685: return "vsh -noprompt -auth " + authentication + " " + username
686: + "@" + host + " " + command;
687: }
688:
689: protected String getConnectionCommand() {
690: return "vsftp -noprompt -auth " + authentication + " "
691: + username + "@" + host;
692: }
693:
694: protected Pattern getExpectedDownloadMessage(String source, File to) {
695: return Pattern.compile("Downloading " + to.getName()
696: + " from [^\\s]+");
697: }
698:
699: protected Pattern getExpectedRemoveMessage(String destination) {
700: return Pattern.compile("Removing [^\\s]+");
701: }
702:
703: protected Pattern getExpectedUploadMessage(File source, String to) {
704: return Pattern.compile("Uploading " + source.getName()
705: + " to [^\\s]+");
706: }
707:
708: public String getAuthentication() {
709: return authentication;
710: }
711:
712: public void setAuthentication(String authentication) {
713: this .authentication = authentication;
714: }
715:
716: public String getHost() {
717: return host;
718: }
719:
720: public void setHost(String host) {
721: this .host = host;
722: }
723:
724: public String getUsername() {
725: return username;
726: }
727:
728: public void setUsername(String username) {
729: this .username = username;
730: }
731:
732: private static StringBuffer chomp(StringBuffer str) {
733: if (str == null || str.length() == 0) {
734: return str;
735: }
736: while ("\n".equals(str.substring(str.length() - 1))
737: || "\r".equals(str.substring(str.length() - 1))) {
738: str.setLength(str.length() - 1);
739: }
740: return str;
741: }
742:
743: public String toString() {
744: return getName() + " " + getUsername() + "@" + getHost() + " ("
745: + getAuthentication() + ")";
746: }
747:
748: /**
749: * Sets the reuse connection time. The same connection will be reused if the time here does not
750: * last between two commands. O indicates that the connection should never be reused
751: *
752: * @param time
753: */
754: public void setReuseConnection(long time) {
755: this .reuseConnection = time;
756: }
757:
758: public long getReadTimeout() {
759: return readTimeout;
760: }
761:
762: public void setReadTimeout(long readTimeout) {
763: this.readTimeout = readTimeout;
764: }
765: }
|