/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.sshd.client.subsystem.sftp; import java.io.IOException; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.client.channel.ClientChannel; import org.apache.sshd.client.subsystem.AbstractSubsystemClient; import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions; import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension; import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtensionFactory; import org.apache.sshd.common.SshException; import org.apache.sshd.common.channel.Channel; import org.apache.sshd.common.subsystem.sftp.SftpConstants; import org.apache.sshd.common.subsystem.sftp.SftpException; import org.apache.sshd.common.subsystem.sftp.SftpHelper; import org.apache.sshd.common.subsystem.sftp.SftpUniversalOwnerAndGroup; import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; /** * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public abstract class AbstractSftpClient extends AbstractSubsystemClient implements SftpClient, RawSftpClient { private final Attributes fileOpenAttributes = new Attributes(); private final AtomicReference<Map<String, Object>> parsedExtensionsHolder = new AtomicReference<>(null); protected AbstractSftpClient() { fileOpenAttributes.setType(SftpConstants.SSH_FILEXFER_TYPE_REGULAR); } @Override public Channel getChannel() { return getClientChannel(); } @Override public <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType) { Object instance = getExtension(BuiltinSftpClientExtensions.fromType(extensionType)); if (instance == null) { return null; } else { return extensionType.cast(instance); } } @Override public SftpClientExtension getExtension(String extensionName) { return getExtension(BuiltinSftpClientExtensions.fromName(extensionName)); } protected SftpClientExtension getExtension(SftpClientExtensionFactory factory) { if (factory == null) { return null; } Map<String, byte[]> extensions = getServerExtensions(); Map<String, Object> parsed = getParsedServerExtensions(extensions); return factory.create(this, this, extensions, parsed); } protected Map<String, Object> getParsedServerExtensions() { return getParsedServerExtensions(getServerExtensions()); } protected Map<String, Object> getParsedServerExtensions(Map<String, byte[]> extensions) { Map<String, Object> parsed = parsedExtensionsHolder.get(); if (parsed == null) { parsed = ParserUtils.parse(extensions); if (parsed == null) { parsed = Collections.emptyMap(); } parsedExtensionsHolder.set(parsed); } return parsed; } /** * Sends the specified command, waits for the response and then invokes {@link #checkResponseStatus(int, Buffer)} * @param cmd The command to send * @param request The request {@link Buffer} * @throws IOException If failed to send, receive or check the returned status * @see #send(int, Buffer) * @see #receive(int) * @see #checkResponseStatus(int, Buffer) */ protected void checkCommandStatus(int cmd, Buffer request) throws IOException { int reqId = send(cmd, request); Buffer response = receive(reqId); checkResponseStatus(cmd, response); } /** * Checks if the incoming response is an {@code SSH_FXP_STATUS} one, * and if so whether the substatus is {@code SSH_FX_OK}. * * @param cmd The sent command opcode * @param buffer The received response {@link Buffer} * @throws IOException If response does not carry a status or carries * a bad status code * @see #checkResponseStatus(int, int, int, String, String) */ protected void checkResponseStatus(int cmd, Buffer buffer) throws IOException { int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (type == SftpConstants.SSH_FXP_STATUS) { int substatus = buffer.getInt(); String msg = buffer.getString(); String lang = buffer.getString(); checkResponseStatus(cmd, id, substatus, msg, lang); } else { //noinspection ThrowableResultOfMethodCallIgnored handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_STATUS, id, type, length, buffer); } } /** * @param cmd The sent command opcode * @param id The request id * @param substatus The sub-status value * @param msg The message * @param lang The language * @throws IOException if the sub-status is not {@code SSH_FX_OK} * @see #throwStatusException(int, int, int, String, String) */ protected void checkResponseStatus(int cmd, int id, int substatus, String msg, String lang) throws IOException { if (log.isTraceEnabled()) { log.trace("checkResponseStatus({})[id={}] cmd={} status={} lang={} msg={}", getClientChannel(), id, SftpConstants.getCommandMessageName(cmd), SftpConstants.getStatusName(substatus), lang, msg); } if (substatus != SftpConstants.SSH_FX_OK) { throwStatusException(cmd, id, substatus, msg, lang); } } protected void throwStatusException(int cmd, int id, int substatus, String msg, String lang) throws IOException { throw new SftpException(substatus, msg); } /** * @param cmd Command to be sent * @param request The {@link Buffer} containing the request * @return The received handle identifier * @throws IOException If failed to send/receive or process the response * @see #send(int, Buffer) * @see #receive(int) * @see #checkHandleResponse(int, Buffer) */ protected byte[] checkHandle(int cmd, Buffer request) throws IOException { int reqId = send(cmd, request); Buffer response = receive(reqId); return checkHandleResponse(cmd, response); } protected byte[] checkHandleResponse(int cmd, Buffer buffer) throws IOException { int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (type == SftpConstants.SSH_FXP_HANDLE) { return ValidateUtils.checkNotNullAndNotEmpty(buffer.getBytes(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY); } if (type == SftpConstants.SSH_FXP_STATUS) { int substatus = buffer.getInt(); String msg = buffer.getString(); String lang = buffer.getString(); if (log.isTraceEnabled()) { log.trace("checkHandleResponse({})[id={}] {} - status: {} [{}] {}", getClientChannel(), id, SftpConstants.getCommandMessageName(cmd), SftpConstants.getStatusName(substatus), lang, msg); } throwStatusException(cmd, id, substatus, msg, lang); } return handleUnexpectedHandlePacket(cmd, id, type, length, buffer); } protected byte[] handleUnexpectedHandlePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException { handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_HANDLE, id, type, length, buffer); throw new SshException("No handling for unexpected handle packet id=" + id + ", type=" + SftpConstants.getCommandMessageName(type) + ", length=" + length); } /** * @param cmd Command to be sent * @param request Request {@link Buffer} * @return The decoded response {@code Attributes} * @throws IOException If failed to send/receive or process the response * @see #send(int, Buffer) * @see #receive(int) * @see #checkAttributesResponse(int, Buffer) */ protected Attributes checkAttributes(int cmd, Buffer request) throws IOException { int reqId = send(cmd, request); Buffer response = receive(reqId); return checkAttributesResponse(cmd, response); } protected Attributes checkAttributesResponse(int cmd, Buffer buffer) throws IOException { int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (type == SftpConstants.SSH_FXP_ATTRS) { return readAttributes(buffer); } if (type == SftpConstants.SSH_FXP_STATUS) { int substatus = buffer.getInt(); String msg = buffer.getString(); String lang = buffer.getString(); if (log.isTraceEnabled()) { log.trace("checkAttributesResponse()[id={}] {} - status: {} [{}] {}", getClientChannel(), id, SftpConstants.getCommandMessageName(cmd), SftpConstants.getStatusName(substatus), lang, msg); } throwStatusException(cmd, id, substatus, msg, lang); } return handleUnexpectedAttributesPacket(cmd, id, type, length, buffer); } protected Attributes handleUnexpectedAttributesPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException { IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_ATTRS, id, type, length, buffer); if (err != null) { throw err; } return null; } /** * @param cmd Command to be sent * @param request The request {@link Buffer} * @return The retrieved name * @throws IOException If failed to send/receive or process the response * @see #send(int, Buffer) * @see #receive(int) * @see #checkOneNameResponse(int, Buffer) */ protected String checkOneName(int cmd, Buffer request) throws IOException { int reqId = send(cmd, request); Buffer response = receive(reqId); return checkOneNameResponse(cmd, response); } protected String checkOneNameResponse(int cmd, Buffer buffer) throws IOException { int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (type == SftpConstants.SSH_FXP_NAME) { int len = buffer.getInt(); if (len != 1) { throw new SshException("SFTP error: received " + len + " names instead of 1"); } String name = buffer.getString(); String longName = null; int version = getVersion(); if (version == SftpConstants.SFTP_V3) { longName = buffer.getString(); } Attributes attrs = readAttributes(buffer); Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version); // TODO decide what to do if not-null and not TRUE if (log.isTraceEnabled()) { log.trace("checkOneNameResponse({})[id={}] {} ({})[{}] eol={}: {}", getClientChannel(), id, SftpConstants.getCommandMessageName(cmd), name, longName, indicator, attrs); } return name; } if (type == SftpConstants.SSH_FXP_STATUS) { int substatus = buffer.getInt(); String msg = buffer.getString(); String lang = buffer.getString(); if (log.isTraceEnabled()) { log.trace("checkOneNameResponse({})[id={}] {} status: {} [{}] {}", getClientChannel(), id, SftpConstants.getCommandMessageName(cmd), SftpConstants.getStatusName(substatus), lang, msg); } throwStatusException(cmd, id, substatus, msg, lang); } return handleUnknownOneNamePacket(cmd, id, type, length, buffer); } protected String handleUnknownOneNamePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException { IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer); if (err != null) { throw err; } return null; } protected Attributes readAttributes(Buffer buffer) throws IOException { Attributes attrs = new Attributes(); int flags = buffer.getInt(); int version = getVersion(); if (version == SftpConstants.SFTP_V3) { if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) { attrs.setSize(buffer.getLong()); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) { attrs.owner(buffer.getInt(), buffer.getInt()); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { int perms = buffer.getInt(); attrs.setPermissions(perms); attrs.setType(SftpHelper.permissionsToFileType(perms)); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) { attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags)); attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags)); } } else if (version >= SftpConstants.SFTP_V4) { attrs.setType(buffer.getUByte()); if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) { attrs.setSize(buffer.getLong()); } if ((version >= SftpConstants.SFTP_V6) && ((flags & SftpConstants.SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0)) { @SuppressWarnings("unused") long allocSize = buffer.getLong(); // TODO handle allocation size } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) { attrs.setOwner(buffer.getString()); attrs.setGroup(buffer.getString()); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { attrs.setPermissions(buffer.getInt()); } // update the permissions according to the type int perms = attrs.getPermissions(); perms |= SftpHelper.fileTypeToPermission(attrs.getType()); attrs.setPermissions(perms); if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) { attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags)); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) { attrs.setCreateTime(SftpHelper.readTime(buffer, version, flags)); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) { attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags)); } if ((version >= SftpConstants.SFTP_V6) && (flags & SftpConstants.SSH_FILEXFER_ATTR_CTIME) != 0) { @SuppressWarnings("unused") FileTime attrsChangedTime = SftpHelper.readTime(buffer, version, flags); // TODO the last time the file attributes were changed } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) { attrs.setAcl(SftpHelper.readACLs(buffer, version)); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_BITS) != 0) { @SuppressWarnings("unused") int bits = buffer.getInt(); @SuppressWarnings("unused") int valid = 0xffffffff; if (version >= SftpConstants.SFTP_V6) { valid = buffer.getInt(); } // TODO: handle attrib bits } if (version >= SftpConstants.SFTP_V6) { if ((flags & SftpConstants.SSH_FILEXFER_ATTR_TEXT_HINT) != 0) { @SuppressWarnings("unused") boolean text = buffer.getBoolean(); // TODO: handle text } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MIME_TYPE) != 0) { @SuppressWarnings("unused") String mimeType = buffer.getString(); // TODO: handle mime-type } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_LINK_COUNT) != 0) { @SuppressWarnings("unused") int nlink = buffer.getInt(); // TODO: handle link-count } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) { @SuppressWarnings("unused") String untranslated = buffer.getString(); // TODO: handle untranslated-name } } } else { throw new IllegalStateException("readAttributes - unsupported version: " + version); } if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) { attrs.setExtensions(SftpHelper.readExtensions(buffer)); } return attrs; } protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException { int version = getVersion(); int flagsMask = 0; Collection<Attribute> flags = Objects.requireNonNull(attributes, "No attributes").getFlags(); if (version == SftpConstants.SFTP_V3) { for (Attribute a : flags) { switch (a) { case Size: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE; break; case UidGid: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_UIDGID; break; case Perms: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS; break; case AccessTime: if (flags.contains(Attribute.ModifyTime)) { flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME; } break; case ModifyTime: if (flags.contains(Attribute.AccessTime)) { flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME; } break; case Extensions: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED; break; default: // do nothing } } buffer.putInt(flagsMask); if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) { buffer.putLong(attributes.getSize()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) { buffer.putInt(attributes.getUserId()); buffer.putInt(attributes.getGroupId()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { buffer.putInt(attributes.getPermissions()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) { SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime()); SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime()); } } else if (version >= SftpConstants.SFTP_V4) { for (Attribute a : flags) { switch (a) { case Size: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE; break; case OwnerGroup: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP; break; case Perms: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS; break; case AccessTime: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME; break; case ModifyTime: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME; break; case CreateTime: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_CREATETIME; break; case Acl: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACL; break; case Extensions: flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED; break; default: // do nothing } } buffer.putInt(flagsMask); buffer.putByte((byte) attributes.getType()); if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) { buffer.putLong(attributes.getSize()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) { String owner = attributes.getOwner(); buffer.putString(GenericUtils.isEmpty(owner) ? SftpUniversalOwnerAndGroup.Owner.getName() : owner); String group = attributes.getGroup(); buffer.putString(GenericUtils.isEmpty(group) ? SftpUniversalOwnerAndGroup.Group.getName() : group); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { buffer.putInt(attributes.getPermissions()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) { SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) { SftpHelper.writeTime(buffer, version, flagsMask, attributes.getCreateTime()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) { SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime()); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) { SftpHelper.writeACLs(buffer, version, attributes.getAcl()); } // TODO: for v6+ add CTIME (see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-21) } else { throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version); } if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) { SftpHelper.writeExtensions(buffer, attributes.getExtensions()); } } @Override public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException { if (!isOpen()) { throw new IOException("open(" + path + ")[" + options + "] client is closed"); } /* * Be consistent with FileChannel#open - if no mode specified then READ is assumed */ if (GenericUtils.isEmpty(options)) { options = EnumSet.of(OpenMode.Read); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer.putString(path); int version = getVersion(); int mode = 0; if (version == SftpConstants.SFTP_V3) { for (OpenMode m : options) { switch (m) { case Read: mode |= SftpConstants.SSH_FXF_READ; break; case Write: mode |= SftpConstants.SSH_FXF_WRITE; break; case Append: mode |= SftpConstants.SSH_FXF_APPEND; break; case Create: mode |= SftpConstants.SSH_FXF_CREAT; break; case Truncate: mode |= SftpConstants.SSH_FXF_TRUNC; break; case Exclusive: mode |= SftpConstants.SSH_FXF_EXCL; break; default: // do nothing } } } else { if (version >= SftpConstants.SFTP_V5) { int access = 0; if (options.contains(OpenMode.Read)) { access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; } if (options.contains(OpenMode.Write)) { access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES; } if (options.contains(OpenMode.Append)) { access |= SftpConstants.ACE4_APPEND_DATA; } buffer.putInt(access); } if (options.contains(OpenMode.Create) && options.contains(OpenMode.Exclusive)) { mode |= SftpConstants.SSH_FXF_CREATE_NEW; } else if (options.contains(OpenMode.Create) && options.contains(OpenMode.Truncate)) { mode |= SftpConstants.SSH_FXF_CREATE_TRUNCATE; } else if (options.contains(OpenMode.Create)) { mode |= SftpConstants.SSH_FXF_OPEN_OR_CREATE; } else if (options.contains(OpenMode.Truncate)) { mode |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING; } else { mode |= SftpConstants.SSH_FXF_OPEN_EXISTING; } } buffer.putInt(mode); writeAttributes(buffer, fileOpenAttributes); CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPEN, buffer)); if (log.isTraceEnabled()) { log.trace("open({})[{}] options={}: {}", getClientSession(), path, options, handle); } return handle; } @Override public void close(Handle handle) throws IOException { if (!isOpen()) { throw new IOException("close(" + handle + ") client is closed"); } if (log.isTraceEnabled()) { log.trace("close({}) {}", getClientSession(), handle); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false); buffer.putBytes(id); checkCommandStatus(SftpConstants.SSH_FXP_CLOSE, buffer); } @Override public void remove(String path) throws IOException { if (!isOpen()) { throw new IOException("remove(" + path + ") client is closed"); } if (log.isDebugEnabled()) { log.debug("remove({}) {}", getClientSession(), path); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer.putString(path); checkCommandStatus(SftpConstants.SSH_FXP_REMOVE, buffer); } @Override public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException { if (!isOpen()) { throw new IOException("rename(" + oldPath + " => " + newPath + ")[" + options + "] client is closed"); } if (log.isDebugEnabled()) { log.debug("rename({}) {} => {}", getClientSession(), oldPath, newPath); } Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */, false); buffer.putString(oldPath); buffer.putString(newPath); int numOptions = GenericUtils.size(options); int version = getVersion(); if (version >= SftpConstants.SFTP_V5) { int opts = 0; if (numOptions > 0) { for (CopyMode opt : options) { switch (opt) { case Atomic: opts |= SftpConstants.SSH_FXP_RENAME_ATOMIC; break; case Overwrite: opts |= SftpConstants.SSH_FXP_RENAME_OVERWRITE; break; default: // do nothing } } } buffer.putInt(opts); } else if (numOptions > 0) { throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")" + " - copy options can not be used with this SFTP version: " + options); } checkCommandStatus(SftpConstants.SSH_FXP_RENAME, buffer); } @Override public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException { if (eofSignalled != null) { eofSignalled.set(null); } if (!isOpen()) { throw new IOException("read(" + handle + "/" + fileOffset + ")[" + dstOffset + "/" + len + "] client is closed"); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false); buffer.putBytes(id); buffer.putLong(fileOffset); buffer.putInt(len); return checkData(SftpConstants.SSH_FXP_READ, buffer, dstOffset, dst, eofSignalled); } protected int checkData(int cmd, Buffer request, int dstOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException { if (eofSignalled != null) { eofSignalled.set(null); } int reqId = send(cmd, request); Buffer response = receive(reqId); return checkDataResponse(cmd, response, dstOffset, dst, eofSignalled); } protected int checkDataResponse(int cmd, Buffer buffer, int dstoff, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException { if (eofSignalled != null) { eofSignalled.set(null); } int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (type == SftpConstants.SSH_FXP_DATA) { int len = buffer.getInt(); buffer.getRawBytes(dst, dstoff, len); Boolean indicator = SftpHelper.getEndOfFileIndicatorValue(buffer, getVersion()); if (log.isTraceEnabled()) { log.trace("checkDataResponse({}][id={}] {} offset={}, len={}, EOF={}", getClientChannel(), SftpConstants.getCommandMessageName(cmd), id, dstoff, len, indicator); } if (eofSignalled != null) { eofSignalled.set(indicator); } return len; } if (type == SftpConstants.SSH_FXP_STATUS) { int substatus = buffer.getInt(); String msg = buffer.getString(); String lang = buffer.getString(); if (log.isTraceEnabled()) { log.trace("checkDataResponse({})[id={}] {} status: {} [{}] {}", getClientChannel(), id, SftpConstants.getCommandMessageName(cmd), SftpConstants.getStatusName(substatus), lang, msg); } if (substatus == SftpConstants.SSH_FX_EOF) { return -1; } throwStatusException(cmd, id, substatus, msg, lang); } return handleUnknownDataPacket(cmd, id, type, length, buffer); } protected int handleUnknownDataPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException { IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_DATA, id, type, length, buffer); if (err != null) { throw err; } return 0; } @Override public void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException { // do some bounds checking first if ((fileOffset < 0) || (srcOffset < 0) || (len < 0)) { throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters " + " are non-negative values: file-offset=" + fileOffset + ", src-offset=" + srcOffset + ", len=" + len); } if ((srcOffset + len) > src.length) { throw new IllegalArgumentException("write(" + handle + ")" + " cannot read bytes " + srcOffset + " to " + (srcOffset + len) + " when array is only of length " + src.length); } if (!isOpen()) { throw new IOException("write(" + handle + "/" + fileOffset + ")[" + srcOffset + "/" + len + "] client is closed"); } if (log.isTraceEnabled()) { log.trace("write({}) handle={}, file-offset={}, buf-offset={}, len={}", getClientChannel(), handle, fileOffset, srcOffset, len); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + len + Long.SIZE /* some extra fields */, false); buffer.putBytes(id); buffer.putLong(fileOffset); buffer.putBytes(src, srcOffset, len); checkCommandStatus(SftpConstants.SSH_FXP_WRITE, buffer); } @Override public void mkdir(String path) throws IOException { if (!isOpen()) { throw new IOException("mkdir(" + path + ") client is closed"); } if (log.isDebugEnabled()) { log.debug("mkdir({}) {}", getClientSession(), path); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer.putString(path); buffer.putInt(0); int version = getVersion(); if (version != SftpConstants.SFTP_V3) { buffer.putByte((byte) 0); } checkCommandStatus(SftpConstants.SSH_FXP_MKDIR, buffer); } @Override public void rmdir(String path) throws IOException { if (!isOpen()) { throw new IOException("rmdir(" + path + ") client is closed"); } if (log.isDebugEnabled()) { log.debug("rmdir({}) {}", getClientSession(), path); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer.putString(path); checkCommandStatus(SftpConstants.SSH_FXP_RMDIR, buffer); } @Override public CloseableHandle openDir(String path) throws IOException { if (!isOpen()) { throw new IOException("openDir(" + path + ") client is closed"); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer.putString(path); CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPENDIR, buffer)); if (log.isTraceEnabled()) { log.trace("openDir({})[{}}: {}", getClientSession(), path, handle); } return handle; } @Override public List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException { if (eolIndicator != null) { eolIndicator.set(null); // assume unknown information } if (!isOpen()) { throw new IOException("readDir(" + handle + ") client is closed"); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* some extra fields */, false); buffer.putBytes(id); int cmdId = send(SftpConstants.SSH_FXP_READDIR, buffer); Buffer response = receive(cmdId); return checkDirResponse(SftpConstants.SSH_FXP_READDIR, response, eolIndicator); } protected List<DirEntry> checkDirResponse(int cmd, Buffer buffer, AtomicReference<Boolean> eolIndicator) throws IOException { if (eolIndicator != null) { eolIndicator.set(null); // assume unknown } int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (type == SftpConstants.SSH_FXP_NAME) { int len = buffer.getInt(); int version = getVersion(); ClientChannel channel = getClientChannel(); if (log.isDebugEnabled()) { log.debug("checkDirResponse({}}[id={}] reading {} entries", channel, id, len); } List<DirEntry> entries = new ArrayList<>(len); for (int i = 0; i < len; i++) { String name = buffer.getString(); String longName = (version == SftpConstants.SFTP_V3) ? buffer.getString() : null; Attributes attrs = readAttributes(buffer); if (log.isTraceEnabled()) { log.trace("checkDirResponse({})[id={}][{}] ({})[{}]: {}", channel, id, i, name, longName, attrs); } entries.add(new DirEntry(name, longName, attrs)); } Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version); if (eolIndicator != null) { eolIndicator.set(indicator); } if (log.isDebugEnabled()) { log.debug("checkDirResponse({}}[id={}] read count={}, eol={}", channel, entries.size(), indicator); } return entries; } if (type == SftpConstants.SSH_FXP_STATUS) { int substatus = buffer.getInt(); String msg = buffer.getString(); String lang = buffer.getString(); if (log.isTraceEnabled()) { log.trace("checkDirResponse({})[id={}] - status: {} [{}] {}", getClientChannel(), id, SftpConstants.getStatusName(substatus), lang, msg); } if (substatus == SftpConstants.SSH_FX_EOF) { return null; } throwStatusException(cmd, id, substatus, msg, lang); } return handleUnknownDirListingPacket(cmd, id, type, length, buffer); } protected List<DirEntry> handleUnknownDirListingPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException { IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer); if (err != null) { throw err; } return Collections.emptyList(); } protected IOException handleUnexpectedPacket(int cmd, int expected, int id, int type, int length, Buffer buffer) throws IOException { throw new SshException("Unexpected SFTP packet received while awaiting " + SftpConstants.getCommandMessageName(expected) + " response to " + SftpConstants.getCommandMessageName(cmd) + ": type=" + SftpConstants.getCommandMessageName(type) + ", id=" + id + ", length=" + length); } @Override public String canonicalPath(String path) throws IOException { if (!isOpen()) { throw new IOException("canonicalPath(" + path + ") client is closed"); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false); buffer.putString(path); return checkOneName(SftpConstants.SSH_FXP_REALPATH, buffer); } @Override public Attributes stat(String path) throws IOException { if (!isOpen()) { throw new IOException("stat(" + path + ") client is closed"); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false); buffer.putString(path); int version = getVersion(); if (version >= SftpConstants.SFTP_V4) { buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL); } return checkAttributes(SftpConstants.SSH_FXP_STAT, buffer); } @Override public Attributes lstat(String path) throws IOException { if (!isOpen()) { throw new IOException("lstat(" + path + ") client is closed"); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false); buffer.putString(path); int version = getVersion(); if (version >= SftpConstants.SFTP_V4) { buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL); } return checkAttributes(SftpConstants.SSH_FXP_LSTAT, buffer); } @Override public Attributes stat(Handle handle) throws IOException { if (!isOpen()) { throw new IOException("stat(" + handle + ") client is closed"); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* a bit extra */, false); buffer.putBytes(id); int version = getVersion(); if (version >= SftpConstants.SFTP_V4) { buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL); } return checkAttributes(SftpConstants.SSH_FXP_FSTAT, buffer); } @Override public void setStat(String path, Attributes attributes) throws IOException { if (!isOpen()) { throw new IOException("setStat(" + path + ")[" + attributes + "] client is closed"); } if (log.isDebugEnabled()) { log.debug("setStat({})[{}]: {}", getClientSession(), path, attributes); } Buffer buffer = new ByteArrayBuffer(); buffer.putString(path); writeAttributes(buffer, attributes); checkCommandStatus(SftpConstants.SSH_FXP_SETSTAT, buffer); } @Override public void setStat(Handle handle, Attributes attributes) throws IOException { if (!isOpen()) { throw new IOException("setStat(" + handle + ")[" + attributes + "] client is closed"); } if (log.isDebugEnabled()) { log.debug("setStat({})[{}]: {}", getClientSession(), handle, attributes); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + (2 * Long.SIZE) /* some extras */, false); buffer.putBytes(id); writeAttributes(buffer, attributes); checkCommandStatus(SftpConstants.SSH_FXP_FSETSTAT, buffer); } @Override public String readLink(String path) throws IOException { if (!isOpen()) { throw new IOException("readLink(" + path + ") client is closed"); } Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false); buffer.putString(path); return checkOneName(SftpConstants.SSH_FXP_READLINK, buffer); } @Override public void link(String linkPath, String targetPath, boolean symbolic) throws IOException { if (!isOpen()) { throw new IOException("link(" + linkPath + " => " + targetPath + ")[symbolic=" + symbolic + "] client is closed"); } if (log.isDebugEnabled()) { log.debug("link({})[symbolic={}] {} => {}", getClientSession(), symbolic, linkPath, targetPath); } Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */, false); int version = getVersion(); if (version < SftpConstants.SFTP_V6) { if (!symbolic) { throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version); } buffer.putString(targetPath); buffer.putString(linkPath); checkCommandStatus(SftpConstants.SSH_FXP_SYMLINK, buffer); } else { buffer.putString(targetPath); buffer.putString(linkPath); buffer.putBoolean(symbolic); checkCommandStatus(SftpConstants.SSH_FXP_LINK, buffer); } } @Override public void lock(Handle handle, long offset, long length, int mask) throws IOException { if (!isOpen()) { throw new IOException("lock(" + handle + ")[offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask) + "] client is closed"); } if (log.isDebugEnabled()) { log.debug("lock({})[{}] offset={}, length={}, mask=0x{}", getClientSession(), handle, offset, length, Integer.toHexString(mask)); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false); buffer.putBytes(id); buffer.putLong(offset); buffer.putLong(length); buffer.putInt(mask); checkCommandStatus(SftpConstants.SSH_FXP_BLOCK, buffer); } @Override public void unlock(Handle handle, long offset, long length) throws IOException { if (!isOpen()) { throw new IOException("unlock" + handle + ")[offset=" + offset + ", length=" + length + "] client is closed"); } if (log.isDebugEnabled()) { log.debug("unlock({})[{}] offset={}, length={}", getClientSession(), handle, offset, length); } byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier(); Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false); buffer.putBytes(id); buffer.putLong(offset); buffer.putLong(length); checkCommandStatus(SftpConstants.SSH_FXP_UNBLOCK, buffer); } }