/* * Copyright (C)2009 - SSHJ Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.schmizz.sshj.sftp; import net.schmizz.concurrent.Promise; import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.common.LoggerFactory; import net.schmizz.sshj.common.SSHException; import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.connection.channel.direct.SessionFactory; import org.slf4j.Logger; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; public class SFTPEngine implements Requester, Closeable { public static final int MAX_SUPPORTED_VERSION = 3; public static final int DEFAULT_TIMEOUT_MS = 30 * 1000; // way too long, but it was the original default /** Logger */ protected final LoggerFactory loggerFactory; protected final Logger log; protected volatile int timeoutMs = DEFAULT_TIMEOUT_MS; protected final PathHelper pathHelper; protected final Session.Subsystem sub; protected final PacketReader reader; protected final OutputStream out; protected long reqID; protected int operativeVersion; protected final Map<String, String> serverExtensions = new HashMap<String, String>(); public SFTPEngine(SessionFactory ssh) throws SSHException { this(ssh, PathHelper.DEFAULT_PATH_SEPARATOR); } public SFTPEngine(SessionFactory ssh, String pathSep) throws SSHException { Session session = ssh.startSession(); loggerFactory = session.getLoggerFactory(); log = loggerFactory.getLogger(getClass()); sub = session.startSubsystem("sftp"); out = sub.getOutputStream(); reader = new PacketReader(this); pathHelper = new PathHelper(new PathHelper.Canonicalizer() { @Override public String canonicalize(String path) throws IOException { return SFTPEngine.this.canonicalize(path); } }, pathSep); } public SFTPEngine init() throws IOException { transmit(new SFTPPacket<Request>(PacketType.INIT).putUInt32(MAX_SUPPORTED_VERSION)); final SFTPPacket<Response> response = reader.readPacket(); final PacketType type = response.readType(); if (type != PacketType.VERSION) throw new SFTPException("Expected INIT packet, received: " + type); operativeVersion = response.readUInt32AsInt(); log.debug("Server version {}", operativeVersion); if (MAX_SUPPORTED_VERSION < operativeVersion) throw new SFTPException("Server reported incompatible protocol version: " + operativeVersion); while (response.available() > 0) serverExtensions.put(response.readString(), response.readString()); // Start reader thread reader.start(); return this; } public Session.Subsystem getSubsystem() { return sub; } public int getOperativeProtocolVersion() { return operativeVersion; } public Request newExtendedRequest(String reqName) { return newRequest(PacketType.EXTENDED).putString(reqName); } @Override public PathHelper getPathHelper() { return pathHelper; } @Override public synchronized Request newRequest(PacketType type) { return new Request(type, reqID = reqID + 1 & 0xffffffffL); } @Override public Promise<Response, SFTPException> request(Request req) throws IOException { final Promise<Response, SFTPException> promise = reader.expectResponseTo(req.getRequestID()); log.debug("Sending {}", req); transmit(req); return promise; } private Response doRequest(Request req) throws IOException { return request(req).retrieve(getTimeoutMs(), TimeUnit.MILLISECONDS); } public RemoteFile open(String path, Set<OpenMode> modes, FileAttributes fa) throws IOException { final byte[] handle = doRequest( newRequest(PacketType.OPEN).putString(path, sub.getRemoteCharset()).putUInt32(OpenMode.toMask(modes)).putFileAttributes(fa) ).ensurePacketTypeIs(PacketType.HANDLE).readBytes(); return new RemoteFile(this, path, handle); } public RemoteFile open(String filename, Set<OpenMode> modes) throws IOException { return open(filename, modes, FileAttributes.EMPTY); } public RemoteFile open(String filename) throws IOException { return open(filename, EnumSet.of(OpenMode.READ)); } public RemoteDirectory openDir(String path) throws IOException { final byte[] handle = doRequest( newRequest(PacketType.OPENDIR).putString(path, sub.getRemoteCharset()) ).ensurePacketTypeIs(PacketType.HANDLE).readBytes(); return new RemoteDirectory(this, path, handle); } public void setAttributes(String path, FileAttributes attrs) throws IOException { doRequest( newRequest(PacketType.SETSTAT).putString(path, sub.getRemoteCharset()).putFileAttributes(attrs) ).ensureStatusPacketIsOK(); } public String readLink(String path) throws IOException { if (operativeVersion < 3) throw new SFTPException("READLINK is not supported in SFTPv" + operativeVersion); return readSingleName( doRequest( newRequest(PacketType.READLINK).putString(path, sub.getRemoteCharset()) ), sub.getRemoteCharset()); } public void makeDir(String path, FileAttributes attrs) throws IOException { doRequest(newRequest(PacketType.MKDIR).putString(path, sub.getRemoteCharset()).putFileAttributes(attrs)).ensureStatusPacketIsOK(); } public void makeDir(String path) throws IOException { makeDir(path, FileAttributes.EMPTY); } public void symlink(String linkpath, String targetpath) throws IOException { if (operativeVersion < 3) throw new SFTPException("SYMLINK is not supported in SFTPv" + operativeVersion); doRequest( newRequest(PacketType.SYMLINK).putString(linkpath, sub.getRemoteCharset()).putString(targetpath, sub.getRemoteCharset()) ).ensureStatusPacketIsOK(); } public void remove(String filename) throws IOException { doRequest( newRequest(PacketType.REMOVE).putString(filename, sub.getRemoteCharset()) ).ensureStatusPacketIsOK(); } public void removeDir(String path) throws IOException { doRequest( newRequest(PacketType.RMDIR).putString(path, sub.getRemoteCharset()) ).ensureStatusIs(Response.StatusCode.OK); } public FileAttributes stat(String path) throws IOException { return stat(PacketType.STAT, path); } public FileAttributes lstat(String path) throws IOException { return stat(PacketType.LSTAT, path); } public void rename(String oldPath, String newPath) throws IOException { if (operativeVersion < 1) throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion); doRequest( newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset()) ).ensureStatusPacketIsOK(); } public String canonicalize(String path) throws IOException { return readSingleName( doRequest( newRequest(PacketType.REALPATH).putString(path, sub.getRemoteCharset()) ), sub.getRemoteCharset()); } public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; } public int getTimeoutMs() { return timeoutMs; } @Override public void close() throws IOException { sub.close(); reader.interrupt(); } protected LoggerFactory getLoggerFactory() { return loggerFactory; } protected FileAttributes stat(PacketType pt, String path) throws IOException { return doRequest(newRequest(pt).putString(path, sub.getRemoteCharset())) .ensurePacketTypeIs(PacketType.ATTRS) .readFileAttributes(); } private static byte[] readSingleNameAsBytes(Response res) throws IOException { res.ensurePacketTypeIs(PacketType.NAME); if (res.readUInt32AsInt() == 1) return res.readStringAsBytes(); else throw new SFTPException("Unexpected data in " + res.getType() + " packet"); } /** Using UTF-8 */ protected static String readSingleName(Response res) throws IOException { return readSingleName(res, IOUtils.UTF8); } /** Using any character set */ protected static String readSingleName(Response res, Charset charset) throws IOException { return new String(readSingleNameAsBytes(res), charset); } protected synchronized void transmit(SFTPPacket<Request> payload) throws IOException { final int len = payload.available(); out.write((len >>> 24) & 0xff); out.write((len >>> 16) & 0xff); out.write((len >>> 8) & 0xff); out.write(len & 0xff); out.write(payload.array(), payload.rpos(), len); out.flush(); } }