| /* |
| * 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.sftp.server; |
| |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.SeekableByteChannel; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.AccessDeniedException; |
| import java.nio.file.CopyOption; |
| import java.nio.file.FileAlreadyExistsException; |
| import java.nio.file.FileStore; |
| import java.nio.file.FileSystem; |
| import java.nio.file.Files; |
| import java.nio.file.InvalidPathException; |
| import java.nio.file.LinkOption; |
| import java.nio.file.NoSuchFileException; |
| import java.nio.file.NotDirectoryException; |
| import java.nio.file.Path; |
| import java.nio.file.StandardCopyOption; |
| import java.nio.file.StandardOpenOption; |
| import java.nio.file.attribute.AclEntry; |
| import java.nio.file.attribute.FileTime; |
| import java.nio.file.attribute.GroupPrincipal; |
| import java.nio.file.attribute.PosixFilePermission; |
| import java.nio.file.attribute.UserPrincipal; |
| import java.security.Principal; |
| import java.util.AbstractMap.SimpleImmutableEntry; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.EnumSet; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NavigableMap; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.IntUnaryOperator; |
| |
| import org.apache.sshd.common.FactoryManager; |
| import org.apache.sshd.common.NamedFactory; |
| import org.apache.sshd.common.NamedResource; |
| import org.apache.sshd.common.OptionalFeature; |
| import org.apache.sshd.common.PropertyResolver; |
| import org.apache.sshd.common.PropertyResolverUtils; |
| import org.apache.sshd.common.config.VersionProperties; |
| import org.apache.sshd.common.digest.BuiltinDigests; |
| import org.apache.sshd.common.digest.Digest; |
| import org.apache.sshd.common.session.Session; |
| import org.apache.sshd.common.util.EventListenerUtils; |
| import org.apache.sshd.common.util.GenericUtils; |
| import org.apache.sshd.common.util.MapEntryUtils; |
| import org.apache.sshd.common.util.MapEntryUtils.MapBuilder; |
| import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder; |
| import org.apache.sshd.common.util.NumberUtils; |
| import org.apache.sshd.common.util.OsUtils; |
| import org.apache.sshd.common.util.ValidateUtils; |
| import org.apache.sshd.common.util.buffer.Buffer; |
| import org.apache.sshd.common.util.buffer.BufferUtils; |
| import org.apache.sshd.common.util.io.FileInfoExtractor; |
| import org.apache.sshd.common.util.io.IoUtils; |
| import org.apache.sshd.common.util.logging.AbstractLoggingBean; |
| import org.apache.sshd.server.channel.ChannelSession; |
| import org.apache.sshd.server.session.ServerSession; |
| import org.apache.sshd.sftp.SftpModuleProperties; |
| import org.apache.sshd.sftp.client.SftpClient; |
| import org.apache.sshd.sftp.client.extensions.openssh.OpenSSHLimitsExtensionInfo; |
| import org.apache.sshd.sftp.client.fs.SftpPath; |
| import org.apache.sshd.sftp.client.impl.SftpPathImpl; |
| import org.apache.sshd.sftp.common.SftpConstants; |
| import org.apache.sshd.sftp.common.SftpException; |
| import org.apache.sshd.sftp.common.SftpHelper; |
| import org.apache.sshd.sftp.common.extensions.AclSupportedParser; |
| import org.apache.sshd.sftp.common.extensions.SpaceAvailableExtensionInfo; |
| import org.apache.sshd.sftp.common.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension; |
| import org.apache.sshd.sftp.common.extensions.openssh.FsyncExtensionParser; |
| import org.apache.sshd.sftp.common.extensions.openssh.HardLinkExtensionParser; |
| import org.apache.sshd.sftp.common.extensions.openssh.LSetStatExtensionParser; |
| import org.apache.sshd.sftp.common.extensions.openssh.LimitsExtensionParser; |
| import org.apache.sshd.sftp.common.extensions.openssh.PosixRenameExtensionParser; |
| |
| /** |
| * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> |
| */ |
| @SuppressWarnings("checkstyle:MethodCount") // TODO split this big class and remove the suppression |
| public abstract class AbstractSftpSubsystemHelper |
| extends AbstractLoggingBean |
| implements SftpSubsystemProxy { |
| |
| /** |
| * The default reported supported client extensions (case <U>insensitive</U>) |
| */ |
| public static final NavigableMap<String, OptionalFeature> DEFAULT_SUPPORTED_CLIENT_EXTENSIONS = |
| // TODO text-seek - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt |
| // TODO home-directory - see |
| // http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt |
| NavigableMapBuilder.<String, OptionalFeature> builder() |
| .put(SftpConstants.EXT_VERSION_SELECT, OptionalFeature.TRUE) |
| .put(SftpConstants.EXT_COPY_FILE, OptionalFeature.TRUE) |
| .put(SftpConstants.EXT_MD5_HASH, BuiltinDigests.md5) |
| .put(SftpConstants.EXT_MD5_HASH_HANDLE, BuiltinDigests.md5) |
| .put(SftpConstants.EXT_CHECK_FILE_HANDLE, OptionalFeature.any(BuiltinDigests.VALUES)) |
| .put(SftpConstants.EXT_CHECK_FILE_NAME, OptionalFeature.any(BuiltinDigests.VALUES)) |
| .put(SftpConstants.EXT_COPY_DATA, OptionalFeature.TRUE) |
| .put(SftpConstants.EXT_SPACE_AVAILABLE, OptionalFeature.TRUE) |
| .immutable(); |
| |
| public static final List<OpenSSHExtension> DEFAULT_OPEN_SSH_EXTENSIONS = Collections.unmodifiableList( |
| Arrays.asList( |
| new OpenSSHExtension(FsyncExtensionParser.NAME, "1"), |
| new OpenSSHExtension(HardLinkExtensionParser.NAME, "1"), |
| new OpenSSHExtension(LSetStatExtensionParser.NAME, "1"), |
| new OpenSSHExtension(PosixRenameExtensionParser.NAME, "1"), |
| new OpenSSHExtension(LimitsExtensionParser.NAME, "1"))); |
| |
| public static final List<String> DEFAULT_OPEN_SSH_EXTENSIONS_NAMES = Collections.unmodifiableList( |
| NamedResource.getNameList(DEFAULT_OPEN_SSH_EXTENSIONS)); |
| |
| public static final Set<Integer> DEFAULT_ACL_SUPPORTED_MASK = Collections.unmodifiableSet( |
| new HashSet<>( |
| Arrays.asList( |
| SftpConstants.SSH_ACL_CAP_ALLOW, |
| SftpConstants.SSH_ACL_CAP_DENY, |
| SftpConstants.SSH_ACL_CAP_AUDIT, |
| SftpConstants.SSH_ACL_CAP_ALARM))); |
| |
| private final ChannelSession channelSession; |
| private final UnsupportedAttributePolicy unsupportedAttributePolicy; |
| private final Collection<SftpEventListener> sftpEventListeners = new CopyOnWriteArraySet<>(); |
| private final SftpEventListener sftpEventListenerProxy; |
| private final SftpFileSystemAccessor fileSystemAccessor; |
| private final SftpErrorStatusDataHandler errorStatusDataHandler; |
| |
| protected AbstractSftpSubsystemHelper(ChannelSession channelSession, SftpSubsystemConfigurator configurator) { |
| this.channelSession = Objects.requireNonNull(channelSession, "No channel session provided"); |
| unsupportedAttributePolicy = Objects.requireNonNull(configurator.getUnsupportedAttributePolicy(), |
| "No unsupported attribute policy provided"); |
| fileSystemAccessor = Objects.requireNonNull(configurator.getFileSystemAccessor(), "No file system accessor"); |
| sftpEventListenerProxy = EventListenerUtils.proxyWrapper(SftpEventListener.class, sftpEventListeners); |
| errorStatusDataHandler |
| = Objects.requireNonNull(configurator.getErrorStatusDataHandler(), "No error status data handler"); |
| } |
| |
| @Override |
| public ChannelSession getServerChannelSession() { |
| return channelSession; |
| } |
| |
| @Override |
| public UnsupportedAttributePolicy getUnsupportedAttributePolicy() { |
| return unsupportedAttributePolicy; |
| } |
| |
| @Override |
| public SftpFileSystemAccessor getFileSystemAccessor() { |
| return fileSystemAccessor; |
| } |
| |
| @Override |
| public SftpEventListener getSftpEventListenerProxy() { |
| return sftpEventListenerProxy; |
| } |
| |
| @Override |
| public boolean addSftpEventListener(SftpEventListener listener) { |
| return sftpEventListeners.add(SftpEventListener.validateListener(listener)); |
| } |
| |
| @Override |
| public boolean removeSftpEventListener(SftpEventListener listener) { |
| if (listener == null) { |
| return false; |
| } |
| |
| return sftpEventListeners.remove(SftpEventListener.validateListener(listener)); |
| } |
| |
| @Override |
| public SftpErrorStatusDataHandler getErrorStatusDataHandler() { |
| return errorStatusDataHandler; |
| } |
| |
| /** |
| * @param buffer The {@link Buffer} holding the request |
| * @param id The request id |
| * @param proposal The proposed value |
| * @return A {@link Boolean} indicating whether to accept/reject the proposal. If {@code null} then |
| * rejection response has been sent, otherwise and appropriate response is generated |
| * @throws IOException If failed send an independent rejection response |
| */ |
| protected Boolean validateProposedVersion(Buffer buffer, int id, String proposal) throws IOException { |
| boolean debugEnabled = log.isDebugEnabled(); |
| Session session = getServerSession(); |
| if (debugEnabled) { |
| log.debug("validateProposedVersion({})[id={}] SSH_FXP_EXTENDED(version-select) (version={})", |
| session, id, proposal); |
| } |
| |
| if (GenericUtils.length(proposal) != 1) { |
| return Boolean.FALSE; |
| } |
| |
| char digit = proposal.charAt(0); |
| if ((digit < '0') || (digit > '9')) { |
| return Boolean.FALSE; |
| } |
| |
| int proposed = digit - '0'; |
| Map.Entry<Integer, String> result = checkVersionCompatibility(buffer, id, proposed, SftpConstants.SSH_FX_FAILURE); |
| if (result == null) { // validation failed |
| return null; |
| } |
| |
| int negotiated = result.getKey(); |
| if (negotiated != proposed) { |
| if (debugEnabled) { |
| log.debug( |
| "validateProposedVersion({})[id={}] SSH_FXP_EXTENDED(version-select) proposed={} different than negotiated={}", |
| session, id, proposed, negotiated); |
| } |
| return Boolean.FALSE; |
| } else { |
| return Boolean.TRUE; |
| } |
| } |
| |
| /** |
| * Checks if a proposed version is within supported range. <B>Note:</B> if the user forced a specific value via the |
| * {@link SftpModuleProperties#SFTP_VERSION} property, then it is used to validate the proposed value |
| * |
| * @param buffer The {@link Buffer} containing the request |
| * @param id The SSH message ID to be used to send the failure message if required |
| * @param proposed The proposed version value |
| * @param failureOpcode The failure opcode to send if validation fails |
| * @return A "pair" whose key is the negotiated version and value a {@link String} of comma |
| * separated values representing all the supported versions. {@code null} if validation failed |
| * and an appropriate status message was sent |
| * @throws IOException If failed to send the failure status message |
| */ |
| protected Map.Entry<Integer, String> checkVersionCompatibility( |
| Buffer buffer, int id, int proposed, int failureOpcode) |
| throws IOException { |
| int low = SftpSubsystemEnvironment.LOWER_SFTP_IMPL; |
| int hig = SftpSubsystemEnvironment.HIGHER_SFTP_IMPL; |
| String available = SftpSubsystemEnvironment.ALL_SFTP_IMPL; |
| // check if user wants to use a specific version |
| ServerSession session = getServerSession(); |
| Integer sftpVersion = SftpModuleProperties.SFTP_VERSION.getOrNull(session); |
| if (sftpVersion != null) { |
| int forcedValue = sftpVersion; |
| if ((forcedValue < SftpSubsystemEnvironment.LOWER_SFTP_IMPL) |
| || (forcedValue > SftpSubsystemEnvironment.HIGHER_SFTP_IMPL)) { |
| throw new IllegalStateException( |
| "Forced SFTP version (" + sftpVersion + ") not within supported values: " + available); |
| } |
| hig = sftpVersion; |
| low = hig; |
| available = sftpVersion.toString(); |
| } |
| |
| boolean traceEnabled = log.isTraceEnabled(); |
| if (traceEnabled) { |
| log.trace("checkVersionCompatibility({})[id={}] - proposed={}, available={}", |
| session, id, proposed, available); |
| } |
| |
| /* |
| * According to all drafts, the server responds with a SSH_FXP_VERSION packet, supplying the lowest (!) of its |
| * own and the client's version number. However, if the client is below what we can support it makes no sense to |
| * agree to it. |
| */ |
| if (proposed < low) { |
| sendStatus(prepareReply(buffer), id, failureOpcode, |
| "Proposed version (" + proposed + ") not in supported range: " + available); |
| return null; |
| } |
| |
| // Force the client to use a lower protocol |
| if (proposed > hig) { |
| if (traceEnabled) { |
| log.trace("checkVersionCompatibility({})[id={}] - replace proposed={} with negotiated={} due to available={}", |
| session, id, proposed, hig, available); |
| } |
| proposed = hig; // debug breakpoint |
| } |
| |
| return new SimpleImmutableEntry<>(proposed, available); |
| } |
| |
| /** |
| * Process an SFTP command. If the command throws an exception, the channel will be closed. |
| * |
| * @param buffer the buffer to process |
| * @throws IOException if anything wrong happens |
| */ |
| protected void process(Buffer buffer) throws IOException { |
| ServerSession session = getServerSession(); |
| int length = buffer.getInt(); |
| int type = buffer.getUByte(); |
| int id = buffer.getInt(); |
| if (log.isDebugEnabled()) { |
| log.debug("process({})[length={}, type={}, id={}] processing", |
| session, length, SftpConstants.getCommandMessageName(type), id); |
| } |
| try { |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| listener.received(session, type, id); |
| } catch (IOException | RuntimeException e) { |
| if (type == SftpConstants.SSH_FXP_INIT) { |
| throw e; |
| } |
| sendStatus(prepareReply(buffer), id, e, type); |
| return; |
| } |
| doProcess(buffer, length, type, id); |
| } |
| |
| protected void doProcess(Buffer buffer, int length, int type, int id) throws IOException { |
| switch (type) { |
| case SftpConstants.SSH_FXP_INIT: |
| doInit(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_OPEN: |
| doOpen(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_CLOSE: |
| doClose(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_READ: |
| doRead(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_WRITE: |
| doWrite(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_LSTAT: |
| doLStat(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_FSTAT: |
| doFStat(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_SETSTAT: |
| doSetStat(buffer, id, "", type, null); |
| break; |
| case SftpConstants.SSH_FXP_FSETSTAT: |
| doFSetStat(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_OPENDIR: |
| doOpenDir(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_READDIR: |
| doReadDir(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_REMOVE: |
| doRemove(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_MKDIR: |
| doMakeDirectory(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_RMDIR: |
| doRemoveDirectory(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_REALPATH: |
| doRealPath(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_STAT: |
| doStat(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_RENAME: |
| doRename(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_READLINK: |
| doReadLink(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_SYMLINK: |
| doSymLink(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_LINK: |
| doLink(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_BLOCK: |
| doBlock(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_UNBLOCK: |
| doUnblock(buffer, id); |
| break; |
| case SftpConstants.SSH_FXP_EXTENDED: |
| doExtended(buffer, id); |
| break; |
| default: |
| doUnsupported(buffer, length, type, id); |
| break; |
| } |
| } |
| |
| protected void doUnsupported(Buffer buffer, int length, int type, int id) throws IOException { |
| String name = SftpConstants.getCommandMessageName(type); |
| log.warn("process({})[length={}, type={}, id={}] unknown command", |
| getServerSession(), length, name, id); |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OP_UNSUPPORTED, |
| "Command " + name + " is unsupported or not implemented"); |
| } |
| |
| protected abstract void doInit(Buffer buffer, int id) throws IOException; |
| |
| protected void doVersionSelect(Buffer buffer, int id) throws IOException { |
| String proposed = buffer.getString(); |
| doVersionSelect(buffer, id, proposed); |
| } |
| |
| protected abstract void doVersionSelect(Buffer buffer, int id, String proposed) throws IOException; |
| |
| protected void doOpen(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| /* |
| * Be consistent with FileChannel#open - if no mode specified then READ is assumed |
| */ |
| int access = 0; |
| int version = getVersion(); |
| if (version >= SftpConstants.SFTP_V5) { |
| access = buffer.getInt(); |
| if (access == 0) { |
| access = SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; |
| } |
| } |
| |
| int pflags = buffer.getInt(); |
| |
| if (version < SftpConstants.SFTP_V5) { |
| int flags = pflags == 0 ? SftpConstants.SSH_FXF_READ : pflags; |
| pflags = 0; |
| switch (flags & (SftpConstants.SSH_FXF_READ | SftpConstants.SSH_FXF_WRITE)) { |
| case SftpConstants.SSH_FXF_READ: |
| access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; |
| break; |
| case SftpConstants.SSH_FXF_WRITE: |
| access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES; |
| break; |
| default: |
| access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; |
| access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES; |
| break; |
| } |
| if ((flags & SftpConstants.SSH_FXF_APPEND) != 0) { |
| access |= SftpConstants.ACE4_APPEND_DATA; |
| pflags |= SftpConstants.SSH_FXF_APPEND_DATA | SftpConstants.SSH_FXF_APPEND_DATA_ATOMIC; |
| } |
| if ((flags & SftpConstants.SSH_FXF_CREAT) != 0) { |
| if ((flags & SftpConstants.SSH_FXF_EXCL) != 0) { |
| pflags |= SftpConstants.SSH_FXF_CREATE_NEW; |
| } else if ((flags & SftpConstants.SSH_FXF_TRUNC) != 0) { |
| pflags |= SftpConstants.SSH_FXF_CREATE_TRUNCATE; |
| } else { |
| pflags |= SftpConstants.SSH_FXF_OPEN_OR_CREATE; |
| } |
| } else { |
| if ((flags & SftpConstants.SSH_FXF_TRUNC) != 0) { |
| pflags |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING; |
| } else { |
| pflags |= SftpConstants.SSH_FXF_OPEN_EXISTING; |
| } |
| } |
| } |
| |
| Map<String, Object> attrs = readAttrs(buffer); |
| String handle; |
| try { |
| handle = doOpen(id, path, pflags, access, attrs); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_OPEN, path); |
| return; |
| } |
| |
| sendHandle(prepareReply(buffer), id, handle); |
| } |
| |
| /** |
| * @param id Request id |
| * @param path Path |
| * @param pflags Open mode flags - see {@code SSH_FXF_XXX} flags |
| * @param access Access mode flags - see {@code ACE4_XXX} flags |
| * @param attrs Requested attributes |
| * @return The assigned (opaque) handle |
| * @throws IOException if failed to execute |
| */ |
| protected abstract String doOpen( |
| int id, String path, int pflags, int access, Map<String, Object> attrs) |
| throws IOException; |
| |
| protected <E extends IOException> E signalOpenFailure( |
| int id, String pathValue, Path path, boolean isDir, E thrown) |
| throws IOException { |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| ServerSession session = getServerSession(); |
| if (log.isDebugEnabled()) { |
| log.debug("signalOpenFailure(id={})[{}] signal {} for {}: {}", |
| id, pathValue, thrown.getClass().getSimpleName(), path, thrown.getMessage()); |
| } |
| |
| listener.openFailed(session, pathValue, path, isDir, thrown); |
| return thrown; |
| } |
| |
| protected void doClose(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| try { |
| doClose(id, handle); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_CLOSE, handle); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, "", ""); |
| } |
| |
| protected abstract void doClose(int id, String handle) throws IOException; |
| |
| protected void doRead(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| long offset = buffer.getLong(); |
| int requestedLength = buffer.getInt(); |
| ServerSession session = getServerSession(); |
| int maxAllowed = SftpModuleProperties.MAX_READDATA_PACKET_LENGTH.getRequired(session); |
| int readLen = Math.min(requestedLength, maxAllowed); |
| if (log.isTraceEnabled()) { |
| log.trace("doRead({})[id={}]({})[offset={}] - req={}, max={}, effective={}", |
| session, id, handle, offset, requestedLength, maxAllowed, readLen); |
| } |
| |
| try { |
| // protected against malicious packets |
| ValidateUtils.checkTrue(readLen >= 0, "Illegal requested read length: %d", readLen); |
| |
| buffer = prepareReply(buffer); |
| buffer.ensureCapacity(readLen + Long.SIZE /* the header */, IntUnaryOperator.identity()); |
| |
| buffer.putByte((byte) SftpConstants.SSH_FXP_DATA); |
| buffer.putInt(id); |
| int lenPos = buffer.wpos(); |
| buffer.putUInt(0L); // save room for length |
| |
| AtomicReference<Boolean> eofRef = new AtomicReference<>(); |
| int startPos = buffer.wpos(); |
| int len = doRead(id, handle, offset, readLen, buffer.array(), startPos, eofRef); |
| if (len < 0) { |
| throw new EOFException("Unable to read " + readLen + " bytes from offset=" + offset + " of " + handle); |
| } |
| buffer.wpos(startPos + len); |
| BufferUtils.updateLengthPlaceholder(buffer, lenPos, len); |
| if (len < readLen) { |
| int version = getVersion(); |
| if (version >= SftpConstants.SFTP_V6) { |
| Boolean eof = eofRef.get(); |
| if (eof != null) { |
| buffer.putBoolean(eof); |
| } |
| } |
| } |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_READ, handle, offset, requestedLength); |
| return; |
| } |
| |
| send(buffer); |
| } |
| |
| protected abstract int doRead( |
| int id, String handle, long offset, int length, byte[] data, int doff, AtomicReference<Boolean> eof) |
| throws IOException; |
| |
| protected void doWrite(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| long offset = buffer.getLong(); |
| int length = buffer.getInt(); |
| try { |
| doWrite(id, handle, offset, length, buffer.array(), buffer.rpos(), buffer.available()); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_WRITE, handle, offset, length); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doWrite( |
| int id, String handle, long offset, int length, byte[] data, int doff, int remaining) |
| throws IOException; |
| |
| protected void doLStat(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; |
| int version = getVersion(); |
| if (version >= SftpConstants.SFTP_V4) { |
| flags = buffer.getInt(); |
| } |
| |
| Map<String, ?> attrs; |
| try { |
| attrs = doLStat(id, path, flags); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_LSTAT, path, flags); |
| return; |
| } |
| |
| sendAttrs(prepareReply(buffer), id, attrs); |
| } |
| |
| protected Map<String, Object> doLStat(int id, String path, int flags) throws IOException { |
| Path p = resolveFile(path); |
| if (log.isDebugEnabled()) { |
| log.debug("doLStat({})[id={}] SSH_FXP_LSTAT (path={}[{}], flags=0x{})", |
| getServerSession(), id, path, p, Integer.toHexString(flags)); |
| } |
| |
| /* |
| * SSH_FXP_STAT and SSH_FXP_LSTAT only differ in that SSH_FXP_STAT follows symbolic links on the server, whereas |
| * SSH_FXP_LSTAT does not. |
| */ |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, p, SftpConstants.SSH_FXP_LSTAT, "", false); |
| return resolveFileAttributes(p, flags, options); |
| } |
| |
| protected void doSetStat( |
| Buffer buffer, int id, String extension, int cmd, Boolean followLinks /* null = auto-resolve */) |
| throws IOException { |
| String path = buffer.getString(); |
| Map<String, Object> attrs = readAttrs(buffer); |
| try { |
| doSetStat(id, path, cmd, extension, attrs, followLinks); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_SETSTAT, path); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doSetStat( |
| int id, String path, int cmd, String extension, Map<String, ?> attrs, Boolean followLinks) |
| throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("doSetStat({})[id={}, cmd={}, extension={}] (path={}, attrs={}, followLinks={})", |
| getServerSession(), id, cmd, extension, path, attrs, followLinks); |
| } |
| |
| Path p = resolveFile(path); |
| if (followLinks == null) { |
| followLinks = resolvePathResolutionFollowLinks(cmd, extension, p); |
| } |
| doSetAttributes(cmd, extension, p, attrs, followLinks); |
| } |
| |
| protected void doFStat(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; |
| int version = getVersion(); |
| if (version >= SftpConstants.SFTP_V4) { |
| flags = buffer.getInt(); |
| } |
| |
| Map<String, ?> attrs; |
| try { |
| attrs = doFStat(id, handle, flags); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_FSTAT, handle, flags); |
| return; |
| } |
| |
| sendAttrs(prepareReply(buffer), id, attrs); |
| } |
| |
| protected abstract Map<String, Object> doFStat(int id, String handle, int flags) throws IOException; |
| |
| protected void doFSetStat(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| Map<String, Object> attrs = readAttrs(buffer); |
| try { |
| doFSetStat(id, handle, attrs); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_FSETSTAT, handle, attrs); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doFSetStat(int id, String handle, Map<String, ?> attrs) throws IOException; |
| |
| protected void doOpenDir(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| String handle; |
| |
| try { |
| Path p = resolveNormalizedLocation(path); |
| if (log.isDebugEnabled()) { |
| log.debug("doOpenDir({})[id={}] SSH_FXP_OPENDIR (path={})[{}]", |
| getServerSession(), id, path, p); |
| } |
| |
| LinkOption[] options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_OPENDIR, "", p); |
| handle = doOpenDir(id, path, p, options); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_OPENDIR, path); |
| return; |
| } |
| |
| sendHandle(prepareReply(buffer), id, handle); |
| } |
| |
| protected abstract String doOpenDir( |
| int id, String path, Path p, LinkOption... options) |
| throws IOException; |
| |
| protected abstract void doReadDir(Buffer buffer, int id) throws IOException; |
| |
| protected void doLink(Buffer buffer, int id) throws IOException { |
| String targetPath = buffer.getString(); |
| String linkPath = buffer.getString(); |
| boolean symLink = buffer.getBoolean(); |
| |
| try { |
| if (log.isDebugEnabled()) { |
| log.debug("doLink({})[id={}] SSH_FXP_LINK linkpath={}, targetpath={}, symlink={}", |
| getServerSession(), id, linkPath, targetPath, symLink); |
| } |
| |
| doLink(id, targetPath, linkPath, symLink); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_LINK, targetPath, linkPath, symLink); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doLink(int id, String targetPath, String linkPath, boolean symLink) throws IOException { |
| createLink(id, targetPath, linkPath, symLink); |
| } |
| |
| protected void doSymLink(Buffer buffer, int id) throws IOException { |
| String targetPath = buffer.getString(); |
| String linkPath = buffer.getString(); |
| try { |
| if (log.isDebugEnabled()) { |
| log.debug("doSymLink({})[id={}] SSH_FXP_SYMLINK linkpath={}, targetpath={}", |
| getServerSession(), id, targetPath, linkPath); |
| } |
| doSymLink(id, targetPath, linkPath); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_SYMLINK, targetPath, linkPath); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doSymLink(int id, String targetPath, String linkPath) throws IOException { |
| createLink(id, targetPath, linkPath, true); |
| } |
| |
| protected abstract void createLink( |
| int id, String existingPath, String linkPath, boolean symLink) |
| throws IOException; |
| |
| // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 4.5 |
| protected void doOpenSSHHardLink(Buffer buffer, int id) throws IOException { |
| String srcFile = buffer.getString(); |
| String dstFile = buffer.getString(); |
| |
| try { |
| doOpenSSHHardLink(id, srcFile, dstFile); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_EXTENDED, |
| HardLinkExtensionParser.NAME, srcFile, dstFile); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 4.8 |
| protected void doOpenSSHLimits(Buffer buffer, int id) throws IOException { |
| OpenSSHLimitsExtensionInfo info = getOpenSSHLimitsExtensionInfo(id, getServerChannelSession()); |
| buffer = prepareReply(buffer); |
| buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); |
| buffer.putInt(id); |
| info.encode(buffer); |
| send(buffer); |
| |
| } |
| |
| protected OpenSSHLimitsExtensionInfo getOpenSSHLimitsExtensionInfo(int id, ChannelSession channel) throws IOException { |
| return new OpenSSHLimitsExtensionInfo(channel); |
| } |
| |
| protected void doOpenSSHHardLink(int id, String srcFile, String dstFile) throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("doOpenSSHHardLink({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, dst={})", |
| getServerSession(), id, HardLinkExtensionParser.NAME, srcFile, dstFile); |
| } |
| |
| createLink(id, srcFile, dstFile, false); |
| } |
| |
| protected void doSpaceAvailable(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| SpaceAvailableExtensionInfo info; |
| try { |
| info = doSpaceAvailable(id, path); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_SPACE_AVAILABLE, path); |
| return; |
| } |
| |
| buffer = prepareReply(buffer); |
| buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); |
| buffer.putInt(id); |
| SpaceAvailableExtensionInfo.encode(buffer, info); |
| send(buffer); |
| } |
| |
| protected SpaceAvailableExtensionInfo doSpaceAvailable(int id, String path) throws IOException { |
| Path nrm = resolveNormalizedLocation(path); |
| ServerSession session = getServerSession(); |
| if (log.isDebugEnabled()) { |
| log.debug("doSpaceAvailable({})[id={}] path={}[{}]", session, id, path, nrm); |
| } |
| |
| FileStore store = Files.getFileStore(nrm); |
| if (log.isTraceEnabled()) { |
| log.trace("doSpaceAvailable({})[id={}] path={}[{}] - {}[{}]", |
| session, id, path, nrm, store.name(), store.type()); |
| } |
| |
| return new SpaceAvailableExtensionInfo(store); |
| } |
| |
| protected void doTextSeek(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| long line = buffer.getLong(); |
| try { |
| // TODO : implement text-seek - see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-6.3 |
| doTextSeek(id, handle, line); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_TEXT_SEEK, handle, line); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doTextSeek(int id, String handle, long line) throws IOException; |
| |
| // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 10 |
| protected void doOpenSSHFsync(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| try { |
| doOpenSSHFsync(id, handle); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, FsyncExtensionParser.NAME, handle); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doOpenSSHFsync(int id, String handle) throws IOException; |
| |
| protected void doCheckFileHash(Buffer buffer, int id, String targetType) throws IOException { |
| String target = buffer.getString(); |
| String algList = buffer.getString(); |
| String[] algos = GenericUtils.split(algList, ','); |
| long startOffset = buffer.getLong(); |
| long length = buffer.getLong(); |
| int blockSize = buffer.getInt(); |
| try { |
| buffer = prepareReply(buffer); |
| buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); |
| buffer.putInt(id); |
| buffer.putString(SftpConstants.EXT_CHECK_FILE); |
| doCheckFileHash(id, targetType, target, |
| Arrays.asList(algos), startOffset, length, blockSize, buffer); |
| } catch (Exception e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, targetType, target, |
| algList, startOffset, length, blockSize); |
| return; |
| } |
| |
| send(buffer); |
| } |
| |
| protected void doCheckFileHash( |
| int id, Path file, NamedFactory<? extends Digest> factory, |
| long startOffset, long length, int blockSize, Buffer buffer) |
| throws Exception { |
| ValidateUtils.checkTrue(startOffset >= 0L, "Invalid start offset: %d", startOffset); |
| ValidateUtils.checkTrue(length >= 0L, "Invalid length: %d", length); |
| ValidateUtils.checkTrue( |
| (blockSize == 0) || (blockSize >= SftpConstants.MIN_CHKFILE_BLOCKSIZE), |
| "Invalid block size: %d", blockSize); |
| Objects.requireNonNull(factory, "No digest factory provided"); |
| buffer.putString(factory.getName()); |
| |
| long effectiveLength = length; |
| long totalLength = Files.size(file); |
| if (effectiveLength == 0L) { |
| effectiveLength = totalLength - startOffset; |
| } else { |
| long maxRead = startOffset + length; |
| if (maxRead > totalLength) { |
| effectiveLength = totalLength - startOffset; |
| } |
| } |
| ValidateUtils.checkTrue(effectiveLength > 0L, |
| "Non-positive effective hash data length: %d", effectiveLength); |
| |
| byte[] digestBuf = (blockSize == 0) |
| ? new byte[Math.min((int) effectiveLength, IoUtils.DEFAULT_COPY_SIZE)] |
| : new byte[Math.min((int) effectiveLength, blockSize)]; |
| ByteBuffer wb = ByteBuffer.wrap(digestBuf); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| ServerSession session = getServerSession(); |
| try (SeekableByteChannel channel = accessor.openFile( |
| this, null, file, null, Collections.emptySet())) { |
| channel.position(startOffset); |
| |
| Digest digest = factory.create(); |
| digest.init(); |
| |
| boolean traceEnabled = log.isTraceEnabled(); |
| if (blockSize == 0) { |
| while (effectiveLength > 0L) { |
| int remainLen = Math.min(digestBuf.length, (int) effectiveLength); |
| ByteBuffer bb = wb; |
| if (remainLen < digestBuf.length) { |
| bb = ByteBuffer.wrap(digestBuf, 0, remainLen); |
| } |
| bb.clear(); // prepare for next read |
| |
| int readLen = channel.read(bb); |
| if (readLen < 0) { |
| break; |
| } |
| |
| effectiveLength -= readLen; |
| digest.update(digestBuf, 0, readLen); |
| } |
| |
| byte[] hashValue = digest.digest(); |
| if (traceEnabled) { |
| log.trace("doCheckFileHash({})[{}] offset={}, length={} - algo={}, hash={}", |
| session, file, startOffset, length, |
| digest.getAlgorithm(), BufferUtils.toHex(':', hashValue)); |
| } |
| buffer.putBytes(hashValue); |
| } else { |
| for (int count = 0; effectiveLength > 0L; count++) { |
| int remainLen = Math.min(digestBuf.length, (int) effectiveLength); |
| ByteBuffer bb = wb; |
| if (remainLen < digestBuf.length) { |
| bb = ByteBuffer.wrap(digestBuf, 0, remainLen); |
| } |
| bb.clear(); // prepare for next read |
| |
| int readLen = channel.read(bb); |
| if (readLen < 0) { |
| break; |
| } |
| |
| effectiveLength -= readLen; |
| digest.update(digestBuf, 0, readLen); |
| |
| byte[] hashValue = digest.digest(); // NOTE: this also resets the hash for the next read |
| if (traceEnabled) { |
| log.trace("doCheckFileHash({})({})[{}] offset={}, length={} - algo={}, hash={}", |
| session, file, count, startOffset, length, |
| digest.getAlgorithm(), BufferUtils.toHex(':', hashValue)); |
| } |
| buffer.putBytes(hashValue); |
| } |
| } |
| |
| accessor.closeFile(this, null, file, null, channel, Collections.emptySet()); |
| } |
| } |
| |
| protected void doMD5Hash(Buffer buffer, int id, String targetType) throws IOException { |
| String target = buffer.getString(); |
| long startOffset = buffer.getLong(); |
| long length = buffer.getLong(); |
| byte[] quickCheckHash = buffer.getBytes(); |
| byte[] hashValue; |
| |
| try { |
| hashValue = doMD5Hash(id, targetType, target, startOffset, length, quickCheckHash); |
| if (log.isTraceEnabled()) { |
| log.trace("doMD5Hash({})({})[{}] offset={}, length={}, quick-hash={} - hash={}", |
| getServerSession(), targetType, target, startOffset, length, |
| BufferUtils.toHex(':', quickCheckHash), |
| BufferUtils.toHex(':', hashValue)); |
| } |
| |
| } catch (Exception e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, targetType, target, |
| startOffset, length, quickCheckHash); |
| return; |
| } |
| |
| buffer = prepareReply(buffer); |
| buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); |
| buffer.putInt(id); |
| buffer.putString(targetType); |
| buffer.putBytes(hashValue); |
| send(buffer); |
| } |
| |
| protected abstract byte[] doMD5Hash( |
| int id, String targetType, String target, long startOffset, long length, byte[] quickCheckHash) |
| throws Exception; |
| |
| protected byte[] doMD5Hash( |
| int id, Path path, long startOffset, long length, byte[] quickCheckHash) |
| throws Exception { |
| ValidateUtils.checkTrue(startOffset >= 0L, "Invalid start offset: %d", startOffset); |
| ValidateUtils.checkTrue(length > 0L, "Invalid length: %d", length); |
| if (!BuiltinDigests.md5.isSupported()) { |
| throw new UnsupportedOperationException(BuiltinDigests.md5.getAlgorithm() + " hash not supported"); |
| } |
| |
| Digest digest = BuiltinDigests.md5.create(); |
| digest.init(); |
| |
| long effectiveLength = length; |
| byte[] digestBuf = new byte[(int) Math.min(effectiveLength, SftpConstants.MD5_QUICK_HASH_SIZE)]; |
| ByteBuffer wb = ByteBuffer.wrap(digestBuf); |
| boolean hashMatches = false; |
| byte[] hashValue = null; |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| boolean traceEnabled = log.isTraceEnabled(); |
| ServerSession session = getServerSession(); |
| try (SeekableByteChannel channel = accessor.openFile( |
| this, null, path, null, Collections.emptySet())) { |
| channel.position(startOffset); |
| |
| /* |
| * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt |
| * section 9.1.1: |
| * |
| * If this is a zero length string, the client does not have the data, and is requesting the hash for |
| * reasons other than comparing with a local file. The server MAY return SSH_FX_OP_UNSUPPORTED in this case. |
| */ |
| if (NumberUtils.length(quickCheckHash) <= 0) { |
| // TODO consider limiting it - e.g., if the requested effective length is <= than some (configurable) |
| // threshold |
| hashMatches = true; |
| } else { |
| int readLen = channel.read(wb); |
| if (readLen < 0) { |
| throw new EOFException("EOF while read initial buffer from " + path); |
| } |
| effectiveLength -= readLen; |
| digest.update(digestBuf, 0, readLen); |
| |
| hashValue = digest.digest(); |
| hashMatches = Arrays.equals(quickCheckHash, hashValue); |
| if (hashMatches) { |
| /* |
| * Need to re-initialize the digester due to the Javadoc: |
| * |
| * "The digest method can be called once for a given number of updates. After digest has been |
| * called, the MessageDigest object is reset to its initialized state." |
| */ |
| if (effectiveLength > 0L) { |
| digest = BuiltinDigests.md5.create(); |
| digest.init(); |
| digest.update(digestBuf, 0, readLen); |
| hashValue = null; // start again |
| } |
| } else { |
| if (traceEnabled) { |
| log.trace("doMD5Hash({})({}) offset={}, length={} - quick-hash mismatched expected={}, actual={}", |
| session, path, startOffset, length, |
| BufferUtils.toHex(':', quickCheckHash), |
| BufferUtils.toHex(':', hashValue)); |
| } |
| } |
| } |
| |
| if (hashMatches) { |
| while (effectiveLength > 0L) { |
| int remainLen = Math.min(digestBuf.length, (int) effectiveLength); |
| ByteBuffer bb = wb; |
| if (remainLen < digestBuf.length) { |
| bb = ByteBuffer.wrap(digestBuf, 0, remainLen); |
| } |
| bb.clear(); // prepare for next read |
| |
| int readLen = channel.read(bb); |
| if (readLen < 0) { |
| break; // user may have specified more than we have available |
| } |
| effectiveLength -= readLen; |
| digest.update(digestBuf, 0, readLen); |
| } |
| |
| if (hashValue == null) { // check if did any more iterations after the quick hash |
| hashValue = digest.digest(); |
| } |
| } else { |
| hashValue = GenericUtils.EMPTY_BYTE_ARRAY; |
| } |
| |
| accessor.closeFile(this, null, path, null, channel, Collections.emptySet()); |
| } |
| |
| if (traceEnabled) { |
| log.trace("doMD5Hash({})({}) offset={}, length={} - matches={}, quick={} hash={}", |
| session, path, startOffset, length, hashMatches, |
| BufferUtils.toHex(':', quickCheckHash), |
| BufferUtils.toHex(':', hashValue)); |
| } |
| |
| return hashValue; |
| } |
| |
| protected abstract void doCheckFileHash( |
| int id, String targetType, String target, Collection<String> algos, |
| long startOffset, long length, int blockSize, Buffer buffer) |
| throws Exception; |
| |
| protected void doReadLink(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| Map.Entry<Path, String> link; |
| try { |
| if (log.isDebugEnabled()) { |
| log.debug("doReadLink({})[id={}] SSH_FXP_READLINK path={}", |
| getServerSession(), id, path); |
| } |
| link = doReadLink(id, path); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_READLINK, path); |
| return; |
| } |
| |
| sendLink(prepareReply(buffer), id, link.getKey(), link.getValue()); |
| } |
| |
| /** |
| * |
| * @param id Request identifier |
| * @param path Referenced path |
| * @return A "pair" containing the local link {@link Path} and its referenced symbolic link |
| * @throws IOException If failed to resolve the requested data |
| */ |
| protected SimpleImmutableEntry<Path, String> doReadLink(int id, String path) throws IOException { |
| Path link = resolveFile(path); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| String target = accessor.resolveLinkTarget(this, link); |
| if (log.isDebugEnabled()) { |
| log.debug("doReadLink({})[id={}] path={}[{}]: {}", |
| getServerSession(), id, path, link, target); |
| } |
| return new SimpleImmutableEntry<>(link, target); |
| } |
| |
| protected void doRename(Buffer buffer, int id) throws IOException { |
| String oldPath = buffer.getString(); |
| String newPath = buffer.getString(); |
| int flags = 0; |
| int version = getVersion(); |
| if (version >= SftpConstants.SFTP_V5) { |
| flags = buffer.getInt(); |
| } |
| try { |
| doRename(id, oldPath, newPath, flags); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_RENAME, oldPath, newPath, flags); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doRename(int id, String oldPath, String newPath, int flags) throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("doRename({})[id={}] SSH_FXP_RENAME (oldPath={}, newPath={}, flags=0x{})", |
| getServerSession(), id, oldPath, newPath, Integer.toHexString(flags)); |
| } |
| |
| Collection<CopyOption> opts = Collections.emptyList(); |
| if (flags != 0) { |
| opts = new ArrayList<>(); |
| if ((flags & SftpConstants.SSH_FXP_RENAME_ATOMIC) == SftpConstants.SSH_FXP_RENAME_ATOMIC) { |
| opts.add(StandardCopyOption.ATOMIC_MOVE); |
| } |
| if ((flags & SftpConstants.SSH_FXP_RENAME_OVERWRITE) == SftpConstants.SSH_FXP_RENAME_OVERWRITE) { |
| opts.add(StandardCopyOption.REPLACE_EXISTING); |
| } |
| } |
| |
| doRename(id, oldPath, newPath, opts); |
| } |
| |
| protected void doRename(int id, String oldPath, String newPath, Collection<CopyOption> opts) throws IOException { |
| Path o = resolveFile(oldPath); |
| Path n = resolveFile(newPath); |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| ServerSession session = getServerSession(); |
| |
| listener.moving(session, o, n, opts); |
| try { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.renameFile(this, o, n, opts); |
| } catch (IOException | RuntimeException | Error e) { |
| listener.moved(session, o, n, opts, e); |
| throw e; |
| } |
| listener.moved(session, o, n, opts, null); |
| } |
| |
| // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 3.3 |
| protected void doPosixRename(Buffer buffer, int id) throws IOException { |
| String oldPath = buffer.getString(); |
| String newPath = buffer.getString(); |
| try { |
| int flags = SftpConstants.SSH_FXP_RENAME_ATOMIC | SftpConstants.SSH_FXP_RENAME_OVERWRITE; |
| doRename(id, oldPath, newPath, flags); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, SftpConstants.SSH_FXP_RENAME, |
| oldPath, newPath); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-7 |
| protected void doCopyData(Buffer buffer, int id) throws IOException { |
| String readHandle = buffer.getString(); |
| long readOffset = buffer.getLong(); |
| long readLength = buffer.getLong(); |
| String writeHandle = buffer.getString(); |
| long writeOffset = buffer.getLong(); |
| try { |
| doCopyData(id, readHandle, readOffset, readLength, writeHandle, writeOffset); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_COPY_DATA, |
| readHandle, readOffset, readLength, writeHandle, writeOffset); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doCopyData( |
| int id, String readHandle, long readOffset, long readLength, String writeHandle, long writeOffset) |
| throws IOException; |
| |
| // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-6 |
| protected void doCopyFile(Buffer buffer, int id) throws IOException { |
| String srcFile = buffer.getString(); |
| String dstFile = buffer.getString(); |
| boolean overwriteDestination = buffer.getBoolean(); |
| |
| try { |
| doCopyFile(id, srcFile, dstFile, overwriteDestination); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_COPY_FILE, srcFile, dstFile, overwriteDestination); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doCopyFile( |
| int id, String srcFile, String dstFile, boolean overwriteDestination) |
| throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("doCopyFile({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, dst={}, overwrite=0x{})", |
| getServerSession(), id, SftpConstants.EXT_COPY_FILE, |
| srcFile, dstFile, overwriteDestination); |
| } |
| |
| doCopyFile(id, srcFile, dstFile, |
| overwriteDestination |
| ? Collections.singletonList(StandardCopyOption.REPLACE_EXISTING) |
| : Collections.emptyList()); |
| } |
| |
| protected void doCopyFile( |
| int id, String srcFile, String dstFile, Collection<CopyOption> opts) |
| throws IOException { |
| Path src = resolveFile(srcFile); |
| Path dst = resolveFile(dstFile); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.copyFile(this, src, dst, opts); |
| } |
| |
| protected void doBlock(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| long offset = buffer.getLong(); |
| long length = buffer.getLong(); |
| int mask = buffer.getInt(); |
| |
| try { |
| doBlock(id, handle, offset, length, mask); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_BLOCK, handle, offset, length, mask); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doBlock( |
| int id, String handle, long offset, long length, int mask) |
| throws IOException; |
| |
| protected void doUnblock(Buffer buffer, int id) throws IOException { |
| String handle = buffer.getString(); |
| long offset = buffer.getLong(); |
| long length = buffer.getLong(); |
| try { |
| doUnblock(id, handle, offset, length); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_UNBLOCK, handle, offset, length); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected abstract void doUnblock( |
| int id, String handle, long offset, long length) |
| throws IOException; |
| |
| protected void doStat(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; |
| int version = getVersion(); |
| if (version >= SftpConstants.SFTP_V4) { |
| flags = buffer.getInt(); |
| } |
| |
| Map<String, Object> attrs; |
| try { |
| attrs = doStat(id, path, flags); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_STAT, path, flags); |
| return; |
| } |
| |
| sendAttrs(prepareReply(buffer), id, attrs); |
| } |
| |
| protected Map<String, Object> doStat(int id, String path, int flags) throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("doStat({})[id={}] SSH_FXP_STAT (path={}, flags=0x{})", |
| getServerSession(), id, path, Integer.toHexString(flags)); |
| } |
| |
| /* |
| * SSH_FXP_STAT and SSH_FXP_LSTAT only differ in that SSH_FXP_STAT follows symbolic links on the server, whereas |
| * SSH_FXP_LSTAT does not. |
| */ |
| Path p = resolveFile(path); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, p, SftpConstants.SSH_FXP_STAT, "", true); |
| return resolveFileAttributes(p, flags, options); |
| } |
| |
| protected void doRealPath(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| boolean debugEnabled = log.isDebugEnabled(); |
| ServerSession session = getServerSession(); |
| if (debugEnabled) { |
| log.debug("doRealPath({})[id={}] SSH_FXP_REALPATH (path={})", session, id, path); |
| } |
| path = GenericUtils.trimToEmpty(path); |
| if (GenericUtils.isEmpty(path)) { |
| path = "."; |
| } |
| |
| Map<String, ?> attrs = Collections.emptyMap(); |
| Map.Entry<Path, Boolean> result; |
| try { |
| int version = getVersion(); |
| if (version < SftpConstants.SFTP_V6) { |
| /* |
| * See http://www.openssh.com/txt/draft-ietf-secsh-filexfer-02.txt: |
| * |
| * The SSH_FXP_REALPATH request can be used to have the server canonicalize any given path name to an |
| * absolute path. |
| * |
| * See also SSHD-294 |
| */ |
| Path p = resolveFile(path); |
| LinkOption[] options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); |
| result = doRealPathV345(id, path, p, options); |
| } else { |
| /* |
| * See https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9 |
| * |
| * This field is optional, and if it is not present in the packet, it is assumed to be |
| * SSH_FXP_REALPATH_NO_CHECK. |
| */ |
| int control = SftpConstants.SSH_FXP_REALPATH_NO_CHECK; |
| if (buffer.available() > 0) { |
| control = buffer.getUByte(); |
| if (debugEnabled) { |
| log.debug("doRealPath({}) - control=0x{} for path={}", |
| session, Integer.toHexString(control), path); |
| } |
| } |
| |
| Collection<String> extraPaths = new LinkedList<>(); |
| while (buffer.available() > 0) { |
| extraPaths.add(buffer.getString()); |
| } |
| |
| Path p = resolveFile(path); |
| LinkOption[] options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); |
| result = doRealPathV6(id, path, extraPaths, p, options); |
| |
| p = result.getKey(); |
| options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); |
| Boolean status = result.getValue(); |
| switch (control) { |
| case SftpConstants.SSH_FXP_REALPATH_STAT_IF: |
| if (status == null) { |
| attrs = handleUnknownStatusFileAttributes( |
| p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); |
| } else if (status) { |
| try { |
| attrs = getAttributes(p, options); |
| } catch (IOException e) { |
| debug("doRealPath({}) - failed ({}) to retrieve attributes of {}: {}", |
| session, e.getClass().getSimpleName(), p, e.getMessage(), e); |
| } |
| } else { |
| if (debugEnabled) { |
| log.debug("doRealPath({}) - dummy attributes for non-existing file: {}", session, p); |
| } |
| } |
| break; |
| case SftpConstants.SSH_FXP_REALPATH_STAT_ALWAYS: |
| if (status == null) { |
| attrs = handleUnknownStatusFileAttributes( |
| p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); |
| } else if (status) { |
| attrs = getAttributes(p, options); |
| } else { |
| throw new NoSuchFileException(p.toString(), p.toString(), "Real path N/A for target"); |
| } |
| break; |
| case SftpConstants.SSH_FXP_REALPATH_NO_CHECK: |
| break; |
| default: |
| log.warn("doRealPath({}) unknown control value 0x{} for path={}", |
| session, Integer.toHexString(control), p); |
| } |
| } |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_REALPATH, path); |
| return; |
| } |
| |
| sendPath(prepareReply(buffer), id, result.getKey(), attrs); |
| } |
| |
| protected SimpleImmutableEntry<Path, Boolean> doRealPathV6( |
| int id, String path, Collection<String> extraPaths, Path p, LinkOption... options) |
| throws IOException { |
| int numExtra = GenericUtils.size(extraPaths); |
| if (numExtra > 0) { |
| if (log.isDebugEnabled()) { |
| log.debug("doRealPathV6({})[id={}] path={}, extra={}", |
| getServerSession(), id, path, extraPaths); |
| } |
| StringBuilder sb = new StringBuilder(GenericUtils.length(path) + numExtra * 8); |
| sb.append(path); |
| |
| for (String p2 : extraPaths) { |
| p = p.resolve(p2); |
| options = getPathResolutionLinkOption( |
| SftpConstants.SSH_FXP_REALPATH, "", p); |
| sb.append('/').append(p2); |
| } |
| |
| path = sb.toString(); |
| } |
| |
| return validateRealPath(id, path, p, options); |
| } |
| |
| protected SimpleImmutableEntry<Path, Boolean> doRealPathV345( |
| int id, String path, Path p, LinkOption... options) |
| throws IOException { |
| return validateRealPath(id, path, p, options); |
| } |
| |
| /** |
| * @param id The request identifier |
| * @param path The original path |
| * @param f The resolve {@link Path} |
| * @param options The {@link LinkOption}s to use to verify file existence and access |
| * @return A {@link SimpleImmutableEntry} whose key is the <U>absolute <B>normalized</B></U> |
| * {@link Path} and value is a {@link Boolean} indicating its status |
| * @throws IOException If failed to validate the file |
| * @see IoUtils#checkFileExists(Path, LinkOption...) |
| */ |
| protected SimpleImmutableEntry<Path, Boolean> validateRealPath( |
| int id, String path, Path f, LinkOption... options) |
| throws IOException { |
| Path p = normalize(f); |
| Boolean status = IoUtils.checkFileExists(p, options); |
| return new SimpleImmutableEntry<>(p, status); |
| } |
| |
| protected void doRemoveDirectory(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| try { |
| doRemoveDirectory(id, path, false); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_RMDIR, path); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doRemoveDirectory(int id, String path, boolean followLinks) throws IOException { |
| Path p = resolveFile(path); |
| if (log.isDebugEnabled()) { |
| log.debug("doRemoveDirectory({})[id={}] SSH_FXP_RMDIR (path={})[{}]", getServerSession(), id, path, p); |
| } |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, p, SftpConstants.SSH_FXP_RMDIR, "", followLinks); |
| if (Files.isDirectory(p, options)) { |
| doRemove(id, p, true); |
| } else { |
| throw signalRemovalPreConditionFailure( |
| id, path, p, new NotDirectoryException(p.toString()), true); |
| } |
| } |
| |
| /** |
| * Called when need to delete a file / directory - also informs the {@link SftpEventListener} |
| * |
| * @param id Deletion request ID |
| * @param p {@link Path} to delete |
| * @param isDirectory Whether the requested path represents a directory or a regular file |
| * @throws IOException If failed to delete |
| */ |
| protected void doRemove(int id, Path p, boolean isDirectory) throws IOException { |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| ServerSession session = getServerSession(); |
| |
| listener.removing(session, p, isDirectory); |
| try { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.removeFile(this, p, isDirectory); |
| } catch (IOException | RuntimeException | Error e) { |
| listener.removed(session, p, isDirectory, e); |
| throw e; |
| } |
| listener.removed(session, p, isDirectory, null); |
| } |
| |
| protected void doMakeDirectory(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| Map<String, ?> attrs = readAttrs(buffer); |
| try { |
| doMakeDirectory(id, path, attrs, false); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, |
| SftpConstants.SSH_FXP_MKDIR, path, attrs); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doMakeDirectory( |
| int id, String path, Map<String, ?> attrs, boolean followLinks) |
| throws IOException { |
| Path resolvedPath = resolveFile(path); |
| ServerSession session = getServerSession(); |
| if (log.isDebugEnabled()) { |
| log.debug("doMakeDirectory({})[id={}] SSH_FXP_MKDIR (path={}[{}], attrs={})", |
| session, id, path, resolvedPath, attrs); |
| } |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, resolvedPath, SftpConstants.SSH_FXP_MKDIR, "", followLinks); |
| SftpPathImpl.withAttributeCache(resolvedPath, p -> { |
| Boolean status = IoUtils.checkFileExists(p, options); |
| if (status == null) { |
| throw new AccessDeniedException(p.toString(), p.toString(), "Cannot validate make-directory existence"); |
| } |
| |
| if (status) { |
| if (Files.isDirectory(p, options)) { |
| throw new FileAlreadyExistsException(p.toString(), p.toString(), "Target directory already exists"); |
| } else { |
| throw new FileAlreadyExistsException(p.toString(), p.toString(), "Already exists as a file"); |
| } |
| } |
| return null; |
| }); |
| // Directory does not exist yet |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| listener.creating(session, resolvedPath, attrs); |
| try { |
| accessor.createDirectory(this, resolvedPath); |
| followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath); |
| doSetAttributes(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath, attrs, followLinks); |
| } catch (IOException | RuntimeException | Error e) { |
| listener.created(session, resolvedPath, attrs, e); |
| throw e; |
| } |
| listener.created(session, resolvedPath, attrs, null); |
| } |
| |
| protected void doRemove(Buffer buffer, int id) throws IOException { |
| String path = buffer.getString(); |
| try { |
| /* |
| * If 'filename' is a symbolic link, the link is removed, not the file it points to. |
| */ |
| doRemoveFile(id, path, false); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_REMOVE, path); |
| return; |
| } |
| |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); |
| } |
| |
| protected void doRemoveFile(int id, String path, boolean followLinks) throws IOException { |
| Path resolvedPath = resolveFile(path); |
| if (log.isDebugEnabled()) { |
| log.debug("doRemoveFile({})[id={}] SSH_FXP_REMOVE (path={}[{}])", getServerSession(), id, path, resolvedPath); |
| } |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, resolvedPath, SftpConstants.SSH_FXP_REMOVE, "", followLinks); |
| SftpPathImpl.withAttributeCache(resolvedPath, p -> { |
| Boolean status = IoUtils.checkFileExists(p, options); |
| if (status == null) { |
| throw signalRemovalPreConditionFailure(id, path, p, |
| new AccessDeniedException(p.toString(), p.toString(), "Cannot determine existence of remove candidate"), |
| false); |
| } else if (!status) { |
| throw signalRemovalPreConditionFailure(id, path, p, |
| new NoSuchFileException(p.toString(), p.toString(), "Removal candidate not found"), false); |
| } else if (Files.isDirectory(p, options)) { |
| throw signalRemovalPreConditionFailure(id, path, p, |
| new SftpException(SftpConstants.SSH_FX_FILE_IS_A_DIRECTORY, p.toString() + " is a folder"), false); |
| } |
| return null; |
| }); |
| // File exists and is not a directory |
| doRemove(id, resolvedPath, false); |
| } |
| |
| protected <E extends IOException> E signalRemovalPreConditionFailure( |
| int id, String pathValue, Path path, E thrown, boolean isRemoveDirectory) |
| throws IOException { |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| ServerSession session = getServerSession(); |
| if (log.isDebugEnabled()) { |
| log.debug("signalRemovalPreConditionFailure(id={})[{}] signal {} for (directory={}) {}: {}", |
| id, pathValue, thrown.getClass().getSimpleName(), isRemoveDirectory, path, thrown.getMessage()); |
| } |
| |
| listener.removing(session, path, isRemoveDirectory); |
| listener.removed(session, path, isRemoveDirectory, thrown); |
| return thrown; |
| } |
| |
| protected void doExtended(Buffer buffer, int id) throws IOException { |
| String extension = buffer.getString(); |
| try { |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| ServerSession session = getServerSession(); |
| listener.receivedExtension(session, extension, id); |
| } catch (IOException | RuntimeException e) { |
| sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_EXTENDED, extension); |
| return; |
| } |
| executeExtendedCommand(buffer, id, extension); |
| } |
| |
| /** |
| * @param buffer The command {@link Buffer} |
| * @param id The request id |
| * @param extension The extension name |
| * @throws IOException If failed to execute the extension |
| */ |
| protected void executeExtendedCommand(Buffer buffer, int id, String extension) throws IOException { |
| switch (extension) { |
| case SftpConstants.EXT_TEXT_SEEK: |
| doTextSeek(buffer, id); |
| break; |
| case SftpConstants.EXT_VERSION_SELECT: |
| doVersionSelect(buffer, id); |
| break; |
| case SftpConstants.EXT_COPY_FILE: |
| doCopyFile(buffer, id); |
| break; |
| case SftpConstants.EXT_COPY_DATA: |
| doCopyData(buffer, id); |
| break; |
| case SftpConstants.EXT_MD5_HASH: |
| case SftpConstants.EXT_MD5_HASH_HANDLE: |
| doMD5Hash(buffer, id, extension); |
| break; |
| case SftpConstants.EXT_CHECK_FILE_HANDLE: |
| case SftpConstants.EXT_CHECK_FILE_NAME: |
| doCheckFileHash(buffer, id, extension); |
| break; |
| case FsyncExtensionParser.NAME: |
| doOpenSSHFsync(buffer, id); |
| break; |
| case SftpConstants.EXT_SPACE_AVAILABLE: |
| doSpaceAvailable(buffer, id); |
| break; |
| case HardLinkExtensionParser.NAME: |
| doOpenSSHHardLink(buffer, id); |
| break; |
| case LSetStatExtensionParser.NAME: |
| doSetStat(buffer, id, extension, -1, Boolean.FALSE); |
| break; |
| case PosixRenameExtensionParser.NAME: |
| doPosixRename(buffer, id); |
| break; |
| case LimitsExtensionParser.NAME: |
| doOpenSSHLimits(buffer, id); |
| break; |
| default: |
| doUnsupportedExtension(buffer, id, extension); |
| } |
| } |
| |
| protected void doUnsupportedExtension( |
| Buffer buffer, int id, String extension) |
| throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("executeExtendedCommand({}) received unsupported SSH_FXP_EXTENDED({})", |
| getServerSession(), extension); |
| } |
| sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OP_UNSUPPORTED, |
| "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented"); |
| } |
| |
| protected void appendExtensions(Buffer buffer, String supportedVersions) { |
| ServerSession session = getServerSession(); |
| appendVersionsExtension(buffer, supportedVersions, session); |
| appendNewlineExtension(buffer, session); |
| appendVendorIdExtension(buffer, VersionProperties.getVersionProperties(), session); |
| appendOpenSSHExtensions(buffer, session); |
| appendAclSupportedExtension(buffer, session); |
| |
| Map<String, OptionalFeature> extensions = getSupportedClientExtensions(session); |
| int numExtensions = MapEntryUtils.size(extensions); |
| List<String> extras = (numExtensions <= 0) ? Collections.emptyList() : new ArrayList<>(numExtensions); |
| if (numExtensions > 0) { |
| boolean debugEnabled = log.isDebugEnabled(); |
| for (Map.Entry<String, OptionalFeature> ee : extensions.entrySet()) { |
| String name = ee.getKey(); |
| OptionalFeature f = ee.getValue(); |
| if (!f.isSupported()) { |
| if (debugEnabled) { |
| log.debug("appendExtensions({}) skip unsupported extension={}", session, name); |
| } |
| continue; |
| } |
| |
| extras.add(name); |
| } |
| } |
| |
| appendSupportedExtension(buffer, extras); |
| appendSupported2Extension(buffer, extras); |
| } |
| |
| protected int appendAclSupportedExtension(Buffer buffer, ServerSession session) { |
| Collection<Integer> maskValues = resolveAclSupportedCapabilities(session); |
| int mask = AclSupportedParser.AclCapabilities.constructAclCapabilities(maskValues); |
| if (mask != 0) { |
| if (log.isTraceEnabled()) { |
| log.trace("appendAclSupportedExtension({}) capabilities={}", |
| session, AclSupportedParser.AclCapabilities.decodeAclCapabilities(mask)); |
| } |
| |
| buffer.putString(SftpConstants.EXT_ACL_SUPPORTED); |
| |
| // placeholder for length |
| int lenPos = buffer.wpos(); |
| buffer.putUInt(0L); |
| buffer.putInt(mask); |
| BufferUtils.updateLengthPlaceholder(buffer, lenPos); |
| } |
| |
| return mask; |
| } |
| |
| protected Collection<Integer> resolveAclSupportedCapabilities(ServerSession session) { |
| String override = SftpModuleProperties.ACL_SUPPORTED_MASK.getOrNull(session); |
| if (override == null) { |
| return DEFAULT_ACL_SUPPORTED_MASK; |
| } |
| |
| // empty means not supported |
| if (log.isDebugEnabled()) { |
| log.debug("resolveAclSupportedCapabilities({}) override='{}'", session, override); |
| } |
| |
| if (override.length() == 0) { |
| return Collections.emptySet(); |
| } |
| |
| String[] names = GenericUtils.split(override, ','); |
| Set<Integer> maskValues = new HashSet<>(names.length); |
| for (String n : names) { |
| Integer v = ValidateUtils.checkNotNull( |
| AclSupportedParser.AclCapabilities.getAclCapabilityValue(n), "Unknown ACL capability: %s", n); |
| maskValues.add(v); |
| } |
| |
| return maskValues; |
| } |
| |
| protected List<OpenSSHExtension> appendOpenSSHExtensions(Buffer buffer, ServerSession session) { |
| List<OpenSSHExtension> extList = resolveOpenSSHExtensions(session); |
| if (GenericUtils.isEmpty(extList)) { |
| return extList; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("appendOpenSSHExtensions({}): {}", session, extList); |
| } |
| |
| for (OpenSSHExtension ext : extList) { |
| buffer.putString(ext.getName()); |
| buffer.putString(ext.getVersion()); |
| } |
| |
| return extList; |
| } |
| |
| protected List<OpenSSHExtension> resolveOpenSSHExtensions(ServerSession session) { |
| String value = SftpModuleProperties.OPENSSH_EXTENSIONS.getOrNull(session); |
| if (value == null) { // No override |
| return DEFAULT_OPEN_SSH_EXTENSIONS; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("resolveOpenSSHExtensions({}) override='{}'", session, value); |
| } |
| |
| String[] pairs = GenericUtils.split(value, ','); |
| int numExts = GenericUtils.length(pairs); |
| if (numExts <= 0) { // User does not want to report ANY extensions |
| return Collections.emptyList(); |
| } |
| |
| List<OpenSSHExtension> extList = new ArrayList<>(numExts); |
| for (String nvp : pairs) { |
| nvp = GenericUtils.trimToEmpty(nvp); |
| if (GenericUtils.isEmpty(nvp)) { |
| continue; |
| } |
| |
| int pos = nvp.indexOf('='); |
| ValidateUtils.checkTrue( |
| (pos > 0) && (pos < (nvp.length() - 1)), "Malformed OpenSSH extension spec: %s", nvp); |
| |
| String name = GenericUtils.trimToEmpty(nvp.substring(0, pos)); |
| String version = GenericUtils.trimToEmpty(nvp.substring(pos + 1)); |
| extList.add(new OpenSSHExtension( |
| name, |
| ValidateUtils.checkNotNullAndNotEmpty( |
| version, "No version specified for OpenSSH extension %s", name))); |
| } |
| |
| return extList; |
| } |
| |
| protected Map<String, OptionalFeature> getSupportedClientExtensions(ServerSession session) { |
| String value = SftpModuleProperties.CLIENT_EXTENSIONS.getOrNull(session); |
| if (value == null) { |
| return DEFAULT_SUPPORTED_CLIENT_EXTENSIONS; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("getSupportedClientExtensions({}) override='{}'", session, value); |
| } |
| |
| if (value.length() <= 0) { // means don't report any extensions |
| return Collections.emptyMap(); |
| } |
| |
| if (value.indexOf(',') <= 0) { |
| return Collections.singletonMap(value, OptionalFeature.TRUE); |
| } |
| |
| String[] comps = GenericUtils.split(value, ','); |
| Map<String, OptionalFeature> result = new LinkedHashMap<>(comps.length); |
| for (String c : comps) { |
| result.put(c, OptionalFeature.TRUE); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Appends the "versions" extension to the buffer. <B>Note:</B> if overriding this method make sure you |
| * either do not append anything or use the correct extension name |
| * |
| * @param buffer The {@link Buffer} to append to |
| * @param value The recommended value - ignored if {@code null}/empty |
| * @param session The {@link ServerSession} for which this extension is added |
| * @return The apended value |
| * @see SftpConstants#EXT_VERSIONS |
| */ |
| protected String appendVersionsExtension( |
| Buffer buffer, String value, ServerSession session) { |
| if (GenericUtils.isEmpty(value)) { |
| return value; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("appendVersionsExtension({}) value={}", session, value); |
| } |
| |
| buffer.putString(SftpConstants.EXT_VERSIONS); |
| buffer.putString(value); |
| return value; |
| } |
| |
| /** |
| * Appends the "newline" extension to the buffer. <B>Note:</B> if overriding this method make sure you |
| * either do not append anything or use the correct extension name |
| * |
| * @param buffer The {@link Buffer} to append to |
| * @param session The {@link ServerSession} for which this extension is added |
| * @return The appended value |
| * @see SftpConstants#EXT_NEWLINE |
| * @see #resolveNewlineValue(ServerSession) |
| */ |
| protected String appendNewlineExtension(Buffer buffer, ServerSession session) { |
| String value = resolveNewlineValue(session); |
| if (GenericUtils.isEmpty(value)) { |
| return value; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("appendNewlineExtension({}) value={}", |
| session, BufferUtils.toHex(':', value.getBytes(StandardCharsets.UTF_8))); |
| } |
| |
| buffer.putString(SftpConstants.EXT_NEWLINE); |
| buffer.putString(value); |
| return value; |
| } |
| |
| protected String resolveNewlineValue(ServerSession session) { |
| return SftpModuleProperties.NEWLINE_VALUE.getRequired(session); |
| } |
| |
| /** |
| * Appends the "vendor-id" extension to the buffer. <B>Note:</B> if overriding this method make sure you |
| * either do not append anything or use the correct extension name |
| * |
| * @param buffer The {@link Buffer} to append to |
| * @param versionProperties The currently available version properties - ignored if {@code null}/empty. The code |
| * expects the following values: |
| * <UL> |
| * <LI>{@code groupId} - as the vendor name</LI> |
| * <LI>{@code artifactId} - as the product name</LI> |
| * <LI>{@code version} - as the product version</LI> |
| * </UL> |
| * @param session The {@link ServerSession} for which these properties are added |
| * @return The version properties |
| * @see SftpConstants#EXT_VENDOR_ID |
| * @see <A HREF= |
| * "http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT |
| * 09 - section 4.4</A> |
| */ |
| protected Map<String, ?> appendVendorIdExtension( |
| Buffer buffer, Map<String, ?> versionProperties, ServerSession session) { |
| if (MapEntryUtils.isEmpty(versionProperties)) { |
| return versionProperties; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("appendVendorIdExtension({}): {}", session, versionProperties); |
| } |
| buffer.putString(SftpConstants.EXT_VENDOR_ID); |
| |
| PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver( |
| Collections.unmodifiableMap(versionProperties)); |
| // placeholder for length |
| int lenPos = buffer.wpos(); |
| buffer.putUInt(0L); |
| buffer.putString(resolver.getStringProperty("groupId", getClass().getPackage().getName())); // vendor-name |
| buffer.putString(resolver.getStringProperty("artifactId", getClass().getSimpleName())); // product-name |
| buffer.putString(resolver.getStringProperty("version", FactoryManager.DEFAULT_VERSION)); // product-version |
| buffer.putLong(0L); // product-build-number |
| BufferUtils.updateLengthPlaceholder(buffer, lenPos); |
| return versionProperties; |
| } |
| |
| /** |
| * Appends the "supported" extension to the buffer. <B>Note:</B> if overriding this method make sure you |
| * either do not append anything or use the correct extension name |
| * |
| * @param buffer The {@link Buffer} to append to |
| * @param extras The extra extensions that are available and can be reported - may be {@code null}/empty |
| */ |
| protected void appendSupportedExtension(Buffer buffer, Collection<String> extras) { |
| buffer.putString(SftpConstants.EXT_SUPPORTED); |
| |
| int lenPos = buffer.wpos(); |
| buffer.putUInt(0L); // length placeholder |
| // supported-attribute-mask |
| buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_SIZE | SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS |
| | SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME | SftpConstants.SSH_FILEXFER_ATTR_CREATETIME |
| | SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME | SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP |
| | SftpConstants.SSH_FILEXFER_ATTR_BITS); |
| // TODO: supported-attribute-bits |
| buffer.putUInt(0L); |
| // supported-open-flags |
| buffer.putInt(SftpConstants.SSH_FXF_READ | SftpConstants.SSH_FXF_WRITE | SftpConstants.SSH_FXF_APPEND |
| | SftpConstants.SSH_FXF_CREAT | SftpConstants.SSH_FXF_TRUNC | SftpConstants.SSH_FXF_EXCL); |
| // TODO: supported-access-mask |
| buffer.putUInt(0L); |
| // max-read-size |
| buffer.putUInt(0L); |
| // supported extensions |
| buffer.putStringList(extras, false); |
| |
| BufferUtils.updateLengthPlaceholder(buffer, lenPos); |
| } |
| |
| /** |
| * Appends the "supported2" extension to the buffer. <B>Note:</B> if overriding this method make sure you |
| * either do not append anything or use the correct extension name |
| * |
| * @param buffer The {@link Buffer} to append to |
| * @param extras The extra extensions that are available and can be reported - may be {@code null}/empty |
| * @see SftpConstants#EXT_SUPPORTED |
| * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-10">DRAFT 13 section 5.4</A> |
| */ |
| @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") |
| protected void appendSupported2Extension(Buffer buffer, Collection<String> extras) { |
| buffer.putString(SftpConstants.EXT_SUPPORTED2); |
| |
| int lenPos = buffer.wpos(); |
| buffer.putUInt(0L); // length placeholder |
| // supported-attribute-mask |
| buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_SIZE | SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS |
| | SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME | SftpConstants.SSH_FILEXFER_ATTR_CREATETIME |
| | SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME | SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP |
| | SftpConstants.SSH_FILEXFER_ATTR_BITS); |
| // TODO: supported-attribute-bits |
| buffer.putUInt(0L); |
| // supported-open-flags |
| buffer.putInt(SftpConstants.SSH_FXF_ACCESS_DISPOSITION | SftpConstants.SSH_FXF_APPEND_DATA); |
| // TODO: supported-access-mask |
| buffer.putUInt(0L); |
| // max-read-size |
| buffer.putUInt(0L); |
| // supported-open-block-vector |
| buffer.putShort(0); |
| // supported-block-vector |
| buffer.putShort(0); |
| // attrib-extension-count + attributes name |
| buffer.putStringList(Collections.<String> emptyList(), true); |
| // extension-count + supported extensions |
| buffer.putStringList(extras, true); |
| |
| BufferUtils.updateLengthPlaceholder(buffer, lenPos); |
| } |
| |
| protected void sendHandle(Buffer buffer, int id, String handle) throws IOException { |
| buffer.putByte((byte) SftpConstants.SSH_FXP_HANDLE); |
| buffer.putInt(id); |
| buffer.putString(handle); |
| send(buffer); |
| } |
| |
| protected void sendAttrs(Buffer buffer, int id, Map<String, ?> attributes) throws IOException { |
| buffer.putByte((byte) SftpConstants.SSH_FXP_ATTRS); |
| buffer.putInt(id); |
| writeAttrs(buffer, attributes); |
| send(buffer); |
| } |
| |
| protected void sendLink(Buffer buffer, int id, Path file, String link) throws IOException { |
| buffer.putByte((byte) SftpConstants.SSH_FXP_NAME); |
| buffer.putInt(id); |
| buffer.putUInt(1L); // one response |
| |
| // in case we are running on Windows |
| String unixPath = link.replace(File.separatorChar, '/'); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.putRemoteFileName(this, file, buffer, unixPath, true); |
| |
| /* |
| * As per the spec (https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.10): |
| * |
| * The server will respond with a SSH_FXP_NAME packet containing only one name and a dummy attributes value. |
| */ |
| Map<String, Object> attrs = Collections.emptyMap(); |
| int version = getVersion(); |
| if (version == SftpConstants.SFTP_V3) { |
| String longName = SftpHelper.getLongName(unixPath, attrs); |
| accessor.putRemoteFileName(this, file, buffer, longName, false); |
| } |
| |
| writeAttrs(buffer, attrs); |
| SftpHelper.indicateEndOfNamesList(buffer, getVersion(), getServerSession()); |
| send(buffer); |
| } |
| |
| protected void sendPath( |
| Buffer buffer, int id, Path f, Map<String, ?> attrs) |
| throws IOException { |
| buffer.putByte((byte) SftpConstants.SSH_FXP_NAME); |
| buffer.putInt(id); |
| buffer.putUInt(1L); // one reply |
| |
| String originalPath = f.toString(); |
| // in case we are running on Windows |
| String unixPath = originalPath.replace(File.separatorChar, '/'); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.putRemoteFileName(this, f, buffer, unixPath, true); |
| |
| int version = getVersion(); |
| if (version == SftpConstants.SFTP_V3) { |
| String longName = getLongName(f, getShortName(f), attrs); |
| accessor.putRemoteFileName(this, f, buffer, longName, false); |
| } |
| |
| writeAttrs(buffer, attrs); |
| SftpHelper.indicateEndOfNamesList(buffer, getVersion(), getServerSession()); |
| send(buffer); |
| } |
| |
| /** |
| * @param id Request id |
| * @param handle The (opaque) handle assigned to this directory |
| * @param dir The {@link DirectoryHandle} |
| * @param buffer The {@link Buffer} to write the results |
| * @param maxSize Max. buffer size |
| * @param followLinks Whether to follow symbolic links when querying the directory contents |
| * @return Number of written entries |
| * @throws IOException If failed to generate an entry |
| */ |
| protected int doReadDir( |
| int id, String handle, DirectoryHandle dir, Buffer buffer, int maxSize, boolean followLinks) |
| throws IOException { |
| ServerSession session = getServerSession(); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, dir.getFile(), SftpConstants.SSH_FXP_READDIR, "", followLinks); |
| int nb = 0; |
| Map<String, Path> entries = new TreeMap<>(Comparator.naturalOrder()); |
| while ((dir.isSendDot() || dir.isSendDotDot() || dir.hasNext()) && (buffer.wpos() < maxSize)) { |
| if (dir.isSendDot()) { |
| writeDirEntry(id, dir, entries, buffer, nb, dir.getFile(), ".", options); |
| dir.markDotSent(); // do not send it again |
| } else if (dir.isSendDotDot()) { |
| Path dirPath = dir.getFile(); |
| Path parentPath = dirPath.getParent(); |
| if (parentPath != null) { |
| writeDirEntry(id, dir, entries, buffer, nb, parentPath, "..", options); |
| } |
| dir.markDotDotSent(); // do not send it again |
| } else { |
| Path f = dir.next(); |
| String shortName = getShortName(f); |
| if (f instanceof SftpPath) { |
| SftpClient.Attributes attributes = ((SftpPath) f).getAttributes(); |
| if (attributes != null) { |
| entries.put(shortName, f); |
| writeDirEntry(session, id, buffer, nb, f, shortName, attributes); |
| nb++; |
| continue; |
| } |
| } |
| writeDirEntry(id, dir, entries, buffer, nb, f, shortName, options); |
| } |
| |
| nb++; |
| } |
| |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| listener.readEntries(session, handle, dir, entries); |
| return nb; |
| } |
| |
| protected void writeDirEntry( |
| ServerSession session, int id, Buffer buffer, int index, Path f, String shortName, |
| SftpClient.Attributes attributes) |
| throws IOException { |
| int version = getVersion(); |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.putRemoteFileName(this, f, buffer, shortName, true); |
| |
| if (version == SftpConstants.SFTP_V3) { |
| String longName = getLongName(f, shortName, attributes); |
| accessor.putRemoteFileName(this, f, buffer, longName, false); |
| |
| if (log.isTraceEnabled()) { |
| log.trace("writeDirEntry({}) id={})[{}] - writing entry {} [{}]: {}", session, id, index, shortName, longName, |
| attributes); |
| } |
| } else { |
| if (log.isTraceEnabled()) { |
| log.trace("writeDirEntry({}) id={})[{}] - writing entry {}: {}", session, id, index, shortName, attributes); |
| } |
| } |
| SftpHelper.writeAttributes(buffer, attributes, version); |
| } |
| |
| /** |
| * @param id Request id |
| * @param dir The {@link DirectoryHandle} |
| * @param entries An in / out {@link Map} for updating the written entry - key = short name, value = entry |
| * {@link Path} |
| * @param buffer The {@link Buffer} to write the results |
| * @param index Zero-based index of the entry to be written |
| * @param f The entry {@link Path} |
| * @param shortName The entry short name |
| * @param options The {@link LinkOption}s to use for querying the entry-s attributes |
| * @throws IOException If failed to generate the entry data |
| */ |
| protected void writeDirEntry( |
| int id, DirectoryHandle dir, Map<String, Path> entries, Buffer buffer, |
| int index, Path f, String shortName, LinkOption... options) |
| throws IOException { |
| Map<String, ?> attrs = resolveFileAttributes( |
| f, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); |
| entries.put(shortName, f); |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| ServerSession session = getServerSession(); |
| accessor.putRemoteFileName(this, f, buffer, shortName, true); |
| |
| int version = getVersion(); |
| if (version == SftpConstants.SFTP_V3) { |
| String longName = getLongName(f, shortName, options); |
| accessor.putRemoteFileName(this, f, buffer, longName, false); |
| |
| if (log.isTraceEnabled()) { |
| log.trace("writeDirEntry({} id={})[{}] - {} [{}]: {}", session, id, index, shortName, longName, attrs); |
| } |
| } else { |
| if (log.isTraceEnabled()) { |
| log.trace("writeDirEntry({} id={})[{}] - {}: {}", session, id, index, shortName, attrs); |
| } |
| } |
| |
| writeAttrs(buffer, attrs); |
| } |
| |
| protected String getLongName( |
| Path f, String shortName, LinkOption... options) |
| throws IOException { |
| return getLongName(f, shortName, true, options); |
| } |
| |
| protected String getLongName( |
| Path f, String shortName, boolean sendAttrs, LinkOption... options) |
| throws IOException { |
| Map<String, Object> attributes; |
| if (sendAttrs) { |
| attributes = getAttributes(f, options); |
| } else { |
| attributes = Collections.emptyMap(); |
| } |
| return getLongName(f, shortName, attributes); |
| } |
| |
| protected String getLongName( |
| Path f, String shortName, Map<String, ?> attributes) |
| throws IOException { |
| return SftpHelper.getLongName(shortName, attributes); |
| } |
| |
| protected String getLongName(Path f, String shortName, SftpClient.Attributes attributes) throws IOException { |
| return getLongName(f, shortName, |
| MapBuilder.<String, Object> builder() |
| .put(IoUtils.OWNER_VIEW_ATTR, attributes.getOwner()) |
| .put(IoUtils.GROUP_VIEW_ATTR, attributes.getGroup()) |
| .put(IoUtils.SIZE_VIEW_ATTR, attributes.getSize()) |
| .put(IoUtils.DIRECTORY_VIEW_ATTR, attributes.isDirectory()) |
| .put(IoUtils.SYMLINK_VIEW_ATTR, attributes.isSymbolicLink()) |
| .put(IoUtils.PERMISSIONS_VIEW_ATTR, SftpHelper.permissionsToAttributes(attributes.getPermissions())) |
| .put(IoUtils.LASTMOD_TIME_VIEW_ATTR, attributes.getModifyTime()) |
| .build()); |
| } |
| |
| protected String getShortName(Path f) throws IOException { |
| Path nrm = normalize(f); |
| int count = nrm.getNameCount(); |
| /* |
| * According to the javadoc: |
| * |
| * The number of elements in the path, or 0 if this path only represents a root component |
| */ |
| if (OsUtils.isUNIX()) { |
| Path name = f.getFileName(); |
| if (name == null) { |
| Path p = resolveFile("."); |
| name = p.getFileName(); |
| } |
| |
| if (name == null) { |
| if (count > 0) { |
| name = nrm.getFileName(); |
| } |
| } |
| |
| if (name != null) { |
| return name.toString(); |
| } else { |
| return nrm.toString(); |
| } |
| } else { // need special handling for Windows root drives |
| if (count > 0) { |
| Path name = nrm.getFileName(); |
| return name.toString(); |
| } else { |
| return nrm.toString().replace(File.separatorChar, '/'); |
| } |
| } |
| } |
| |
| protected NavigableMap<String, Object> resolveFileAttributes( |
| Path path, int flags, LinkOption... options) |
| throws IOException { |
| return SftpPathImpl.withAttributeCache(path, file -> { |
| Boolean status = IoUtils.checkFileExists(file, options); |
| if (status == null) { |
| return handleUnknownStatusFileAttributes(file, flags, options); |
| } else if (!status) { |
| throw new NoSuchFileException(file.toString(), file.toString(), "Attributes N/A for target"); |
| } else { |
| return getAttributes(file, flags, options); |
| } |
| }); |
| } |
| |
| protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) throws IOException { |
| SftpHelper.writeAttrs(buffer, getVersion(), attributes); |
| } |
| |
| protected NavigableMap<String, Object> getAttributes(Path file, LinkOption... options) |
| throws IOException { |
| return getAttributes(file, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); |
| } |
| |
| protected NavigableMap<String, Object> handleUnknownStatusFileAttributes( |
| Path file, int flags, LinkOption... options) |
| throws IOException { |
| UnsupportedAttributePolicy policy = getUnsupportedAttributePolicy(); |
| switch (policy) { |
| case Ignore: |
| break; |
| case ThrowException: |
| throw new AccessDeniedException(file.toString(), file.toString(), |
| "Cannot determine existence for attributes of target"); |
| case Warn: |
| log.warn("handleUnknownStatusFileAttributes({})[{}] cannot determine existence", getServerSession(), file); |
| break; |
| default: |
| log.warn("handleUnknownStatusFileAttributes({})[{}] unknown policy: {}", getServerSession(), file, policy); |
| } |
| |
| return getAttributes(file, flags, options); |
| } |
| |
| /** |
| * @param path The {@link Path} location for the required attributes |
| * @param flags A mask of the original required attributes - ignored by the default implementation |
| * @param options The {@link LinkOption}s to use in order to access the file if necessary |
| * @return A {@link Map} of the retrieved attributes |
| * @throws IOException If failed to access the file |
| * @see #resolveReportedFileAttributes(Path, int, LinkOption...) |
| */ |
| protected NavigableMap<String, Object> getAttributes(Path path, int flags, LinkOption... options) |
| throws IOException { |
| NavigableMap<String, Object> attrs |
| = SftpPathImpl.withAttributeCache(path, file -> resolveReportedFileAttributes(file, flags, options)); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| return accessor.resolveReportedFileAttributes(this, path, flags, attrs, options); |
| } |
| |
| protected NavigableMap<String, Object> resolveReportedFileAttributes(Path file, int flags, LinkOption... options) |
| throws IOException { |
| FileSystem fs = file.getFileSystem(); |
| Collection<String> supportedViews = fs.supportedFileAttributeViews(); |
| NavigableMap<String, Object> attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); |
| Collection<String> views; |
| |
| if (GenericUtils.isEmpty(supportedViews)) { |
| views = Collections.emptyList(); |
| } else if (supportedViews.contains("unix")) { |
| views = SftpFileSystemAccessor.DEFAULT_UNIX_VIEW; |
| } else { |
| views = GenericUtils.map(supportedViews, v -> v + ":*"); |
| } |
| |
| for (String v : views) { |
| Map<String, ?> ta = readFileAttributes(file, v, options); |
| if (MapEntryUtils.isNotEmpty(ta)) { |
| attrs.putAll(ta); |
| } |
| } |
| |
| Map<String, ?> completions = resolveMissingFileAttributes(file, flags, attrs, options); |
| if (MapEntryUtils.isNotEmpty(completions)) { |
| attrs.putAll(completions); |
| } |
| return attrs; |
| } |
| |
| /** |
| * Called by {@link #getAttributes(Path, int, LinkOption...)} in order to complete any attributes that could not be |
| * retrieved via the supported file system views. These attributes are deemed important so an extra effort is made |
| * to provide a value for them |
| * |
| * @param file The {@link Path} location for the required attributes |
| * @param flags A mask of the original required attributes - ignored by the default implementation |
| * @param current The {@link Map} of attributes already retrieved - may be {@code null}/empty and/or |
| * unmodifiable |
| * @param options The {@link LinkOption}s to use in order to access the file if necessary |
| * @return A {@link Map} of the extra attributes whose values need to be updated in the original map. |
| * <B>Note:</B> it is allowed to specify values which <U>override</U> existing ones - the |
| * default implementation does not override values that have a non-{@code null} value |
| * @throws IOException If failed to access the attributes - in which case an <U>error</U> is returned to the SFTP |
| * client |
| * @see SftpFileSystemAccessor#FILEATTRS_RESOLVERS |
| */ |
| protected NavigableMap<String, Object> resolveMissingFileAttributes( |
| Path file, int flags, Map<String, Object> current, LinkOption... options) |
| throws IOException { |
| boolean debugEnabled = log.isDebugEnabled(); |
| ServerSession session = getServerSession(); |
| NavigableMap<String, Object> attrs = null; |
| // Cannot use forEach because the attrs variable is not effectively final |
| for (Map.Entry<String, FileInfoExtractor<?>> re : SftpFileSystemAccessor.FILEATTRS_RESOLVERS.entrySet()) { |
| String name = re.getKey(); |
| Object value = MapEntryUtils.isEmpty(current) ? null : current.get(name); |
| FileInfoExtractor<?> x = re.getValue(); |
| try { |
| Object resolved = resolveMissingFileAttributeValue(file, name, value, x, options); |
| if (Objects.equals(resolved, value)) { |
| continue; |
| } |
| |
| if (attrs == null) { |
| attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); |
| } |
| |
| attrs.put(name, resolved); |
| |
| if (debugEnabled) { |
| log.debug("resolveMissingFileAttributes({})[{}[{}]] replace {} with {}", |
| session, file, name, value, resolved); |
| } |
| } catch (IOException e) { |
| warn("resolveMissingFileAttributes({})[{}[{}]] failed ({}) to resolve missing value: {}", |
| session, file, name, e.getClass().getSimpleName(), e.getMessage(), e); |
| } |
| } |
| |
| if (attrs == null) { |
| return Collections.emptyNavigableMap(); |
| } else { |
| return attrs; |
| } |
| } |
| |
| protected Object resolveMissingFileAttributeValue( |
| Path file, String name, Object value, FileInfoExtractor<?> x, LinkOption... options) |
| throws IOException { |
| if (value != null) { |
| return value; |
| } else { |
| return x.infoOf(file, options); |
| } |
| } |
| |
| protected NavigableMap<String, Object> addMissingAttribute( |
| Path file, NavigableMap<String, Object> current, |
| String name, FileInfoExtractor<?> x, LinkOption... options) |
| throws IOException { |
| Object value = MapEntryUtils.isEmpty(current) ? null : current.get(name); |
| if (value != null) { // already have the value |
| return current; |
| } |
| |
| // skip if still no value |
| value = x.infoOf(file, options); |
| if (value == null) { |
| return current; |
| } |
| |
| if (current == null) { |
| current = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); |
| } |
| |
| current.put(name, value); |
| return current; |
| } |
| |
| protected NavigableMap<String, Object> readFileAttributes( |
| Path file, String view, LinkOption... options) |
| throws IOException { |
| try { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| Map<String, ?> attrs = accessor.readFileAttributes(this, file, view, options); |
| if (MapEntryUtils.isEmpty(attrs)) { |
| return Collections.emptyNavigableMap(); |
| } |
| |
| NavigableMap<String, Object> sorted = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); |
| sorted.putAll(attrs); |
| return sorted; |
| } catch (IOException e) { |
| return handleReadFileAttributesException(file, view, options, e); |
| } |
| } |
| |
| protected NavigableMap<String, Object> handleReadFileAttributesException( |
| Path file, String view, LinkOption[] options, IOException e) |
| throws IOException { |
| if (log.isTraceEnabled()) { |
| log.trace("handleReadFileAttributesException(" + file + ")[" + view + "] details", e); |
| } |
| |
| UnsupportedAttributePolicy policy = getUnsupportedAttributePolicy(); |
| switch (policy) { |
| case Ignore: |
| break; |
| case Warn: |
| log.warn("handleReadFileAttributesException({})[{}] {}", file, view, e.toString()); |
| break; |
| case ThrowException: |
| throw e; |
| default: |
| log.warn("handleReadFileAttributesException({})[{}] Unknown policy ({}) for {}", file, view, policy, |
| e.toString()); |
| } |
| |
| return Collections.emptyNavigableMap(); |
| } |
| |
| protected void doSetAttributes( |
| int cmd, String extension, Path file, Map<String, ?> attributes, boolean followLinks) |
| throws IOException { |
| SftpEventListener listener = getSftpEventListenerProxy(); |
| ServerSession session = getServerSession(); |
| listener.modifyingAttributes(session, file, attributes); |
| try { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| LinkOption[] options = accessor.resolveFileAccessLinkOptions( |
| this, file, cmd, extension, followLinks); |
| setFileAttributes(file, attributes, options); |
| } catch (IOException | RuntimeException | Error e) { |
| listener.modifiedAttributes(session, file, attributes, e); |
| throw e; |
| } |
| listener.modifiedAttributes(session, file, attributes, null); |
| } |
| |
| protected LinkOption[] getPathResolutionLinkOption(int cmd, String extension, Path path) throws IOException { |
| boolean followLinks = resolvePathResolutionFollowLinks(cmd, extension, path); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| return accessor.resolveFileAccessLinkOptions(this, path, cmd, extension, followLinks); |
| } |
| |
| protected boolean resolvePathResolutionFollowLinks(int cmd, String extension, Path path) throws IOException { |
| return SftpModuleProperties.AUTO_FOLLOW_LINKS.getRequired(getServerSession()); |
| } |
| |
| protected void setFileAttributes( |
| Path file, Map<String, ?> attributes, LinkOption... options) |
| throws IOException { |
| Set<String> unsupported = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); |
| // Cannot use forEach because of the potential IOException being thrown |
| for (Map.Entry<String, ?> ae : attributes.entrySet()) { |
| String attribute = ae.getKey(); |
| Object value = ae.getValue(); |
| String view = null; |
| switch (attribute) { |
| case IoUtils.SIZE_VIEW_ATTR: { |
| long newSize = ((Number) value).longValue(); |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| Set<StandardOpenOption> openOptions = EnumSet.of(StandardOpenOption.WRITE); |
| try (SeekableByteChannel channel = accessor.openFile(this, null, file, null, openOptions)) { |
| channel.truncate(newSize); |
| accessor.closeFile(this, null, file, null, channel, openOptions); |
| } |
| continue; |
| } |
| case IoUtils.USERID_VIEW_ATTR: |
| view = "unix"; |
| break; |
| case IoUtils.GROUPID_VIEW_ATTR: |
| view = "unix"; |
| break; |
| case IoUtils.OWNER_VIEW_ATTR: |
| view = "posix"; |
| value = toUser(file, (UserPrincipal) value); |
| break; |
| case IoUtils.GROUP_VIEW_ATTR: |
| view = "posix"; |
| value = toGroup(file, (GroupPrincipal) value); |
| break; |
| case IoUtils.PERMISSIONS_VIEW_ATTR: |
| view = "posix"; |
| break; |
| case IoUtils.ACL_VIEW_ATTR: |
| view = "acl"; |
| break; |
| case IoUtils.CREATE_TIME_VIEW_ATTR: |
| view = "basic"; |
| break; |
| case IoUtils.LASTMOD_TIME_VIEW_ATTR: |
| view = "basic"; |
| break; |
| case IoUtils.LASTACC_TIME_VIEW_ATTR: |
| view = "basic"; |
| break; |
| case IoUtils.EXTENDED_VIEW_ATTR: |
| view = "extended"; |
| break; |
| default: // ignored |
| } |
| if ((GenericUtils.length(view) > 0) && (value != null)) { |
| try { |
| setFileAttribute(file, view, attribute, value, options); |
| } catch (Exception e) { |
| handleSetFileAttributeFailure(file, view, attribute, value, unsupported, e); |
| } |
| } |
| } |
| |
| handleUnsupportedAttributes(unsupported); |
| } |
| |
| protected void handleSetFileAttributeFailure( |
| Path file, String view, String attribute, Object value, |
| Collection<String> unsupported, Exception e) |
| throws IOException { |
| boolean debugEnabled = log.isDebugEnabled(); |
| if (e instanceof UnsupportedOperationException) { |
| if (debugEnabled) { |
| log.debug("handleSetFileAttributeFailure({})[{}] {}:{}={} unsupported: {}", |
| getServerSession(), file, view, attribute, value, e.getMessage()); |
| } |
| unsupported.add(attribute); |
| } else { |
| warn("handleSetFileAttributeFailure({})[{}] {}:{}={} - failed ({}) to set: {}", |
| getServerSession(), file, view, attribute, value, e.getClass().getSimpleName(), e.getMessage(), e); |
| if (e instanceof IOException) { |
| throw (IOException) e; |
| } else { |
| throw new IOException(e); |
| } |
| } |
| } |
| |
| protected void setFileAttribute( |
| Path file, String view, String attribute, Object value, LinkOption... options) |
| throws IOException { |
| if (log.isTraceEnabled()) { |
| log.trace("setFileAttribute({})[{}] {}:{}={}", |
| getServerSession(), file, view, attribute, value); |
| } |
| |
| if (IoUtils.ACL_VIEW_ATTR.equalsIgnoreCase(attribute) && "acl".equalsIgnoreCase(view)) { |
| @SuppressWarnings("unchecked") |
| List<AclEntry> acl = (List<AclEntry>) value; |
| setFileAccessControl(file, acl, options); |
| } else if (IoUtils.PERMISSIONS_VIEW_ATTR.equalsIgnoreCase(attribute)) { |
| @SuppressWarnings("unchecked") |
| Set<PosixFilePermission> perms = (Set<PosixFilePermission>) value; |
| setFilePermissions(file, perms, options); |
| } else if (IoUtils.OWNER_VIEW_ATTR.equalsIgnoreCase(attribute) |
| || IoUtils.GROUP_VIEW_ATTR.equalsIgnoreCase(attribute)) { |
| setFileOwnership(file, attribute, (Principal) value, options); |
| } else if (IoUtils.CREATE_TIME_VIEW_ATTR.equalsIgnoreCase(attribute) |
| || IoUtils.LASTMOD_TIME_VIEW_ATTR.equalsIgnoreCase(attribute) |
| || IoUtils.LASTACC_TIME_VIEW_ATTR.equalsIgnoreCase(attribute)) { |
| setFileTime(file, view, attribute, (FileTime) value, options); |
| } else if (IoUtils.EXTENDED_VIEW_ATTR.equalsIgnoreCase(attribute) && "extended".equalsIgnoreCase(view)) { |
| @SuppressWarnings("unchecked") |
| Map<String, byte[]> extensions = (Map<String, byte[]>) value; |
| setFileExtensions(file, extensions, options); |
| } else { |
| setFileRawViewAttribute(file, view, attribute, value, options); |
| } |
| } |
| |
| protected void setFileTime( |
| Path file, String view, String attribute, FileTime value, LinkOption... options) |
| throws IOException { |
| if (value == null) { |
| return; |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug("setFileTime({})[{}] {}:{}={}", |
| getServerSession(), file, view, attribute, value); |
| } |
| |
| setFileRawViewAttribute(file, view, attribute, value, options); |
| } |
| |
| protected void setFileRawViewAttribute( |
| Path file, String view, String attribute, Object value, LinkOption... options) |
| throws IOException { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.setFileAttribute(this, file, view, attribute, value, options); |
| } |
| |
| protected void setFileOwnership( |
| Path file, String attribute, Principal value, LinkOption... options) |
| throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("setFileOwnership({})[{}] {}={}", getServerSession(), file, attribute, value); |
| } |
| |
| /* |
| * Quoting from Javadoc of FileOwnerAttributeView#setOwner: |
| * |
| * To ensure consistent and correct behavior across platforms it is recommended that this method should only be |
| * used to set the file owner to a user principal that is not a group. |
| */ |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| if (IoUtils.OWNER_VIEW_ATTR.equalsIgnoreCase(attribute)) { |
| accessor.setFileOwner(this, file, value, options); |
| } else if (IoUtils.GROUP_VIEW_ATTR.equalsIgnoreCase(attribute)) { |
| accessor.setGroupOwner(this, file, value, options); |
| } else { |
| throw new UnsupportedOperationException("Unknown ownership attribute: " + attribute); |
| } |
| } |
| |
| protected void setFileExtensions( |
| Path file, Map<String, byte[]> extensions, LinkOption... options) |
| throws IOException { |
| /* |
| * According to v3,4,5: |
| * |
| * Implementations SHOULD ignore extended data fields that they do not understand. |
| * |
| * But according to v6 (https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-28): Implementations MUST |
| * return SSH_FX_UNSUPPORTED if there are any unrecognized extensions. |
| */ |
| int version = getVersion(); |
| if (version < SftpConstants.SFTP_V6) { |
| if (MapEntryUtils.isNotEmpty(extensions) && log.isDebugEnabled()) { |
| log.debug("setFileExtensions({})[{}]: {}", getServerSession(), file, extensions.keySet()); |
| } |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.applyExtensionFileAttributes(this, file, extensions, options); |
| } else { |
| if (MapEntryUtils.isEmpty(extensions)) { |
| return; |
| } |
| |
| throw new UnsupportedOperationException("File extensions not supported"); |
| } |
| } |
| |
| protected void setFilePermissions( |
| Path file, Set<PosixFilePermission> perms, LinkOption... options) |
| throws IOException { |
| if (log.isTraceEnabled()) { |
| log.trace("setFilePermissions({})[{}] {}", getServerSession(), file, perms); |
| } |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.setFilePermissions(this, file, perms, options); |
| } |
| |
| protected void setFileAccessControl( |
| Path file, List<AclEntry> acl, LinkOption... options) |
| throws IOException { |
| if (log.isTraceEnabled()) { |
| log.trace("setFileAccessControl({})[{}] {}", getServerSession(), file, acl); |
| } |
| |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| accessor.setFileAccessControl(this, file, acl, options); |
| } |
| |
| protected void handleUnsupportedAttributes(Collection<String> attributes) { |
| if (attributes.isEmpty()) { |
| return; |
| } |
| |
| String attrsList = GenericUtils.join(attributes, ','); |
| UnsupportedAttributePolicy policy = getUnsupportedAttributePolicy(); |
| switch (policy) { |
| case Ignore: |
| break; |
| case Warn: |
| log.warn("Unsupported attributes: {}", attrsList); |
| break; |
| case ThrowException: |
| throw new UnsupportedOperationException("Unsupported attributes: " + attrsList); |
| default: |
| log.warn("Unknown policy ''{}'' for attributes={}", policy, attrsList); |
| } |
| } |
| |
| protected GroupPrincipal toGroup(Path file, GroupPrincipal name) throws IOException { |
| try { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| return accessor.resolveGroupOwner(this, file, name); |
| } catch (IOException e) { |
| handleUserPrincipalLookupServiceException(GroupPrincipal.class, name.toString(), e); |
| return null; |
| } |
| } |
| |
| protected UserPrincipal toUser(Path file, UserPrincipal name) throws IOException { |
| try { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| return accessor.resolveFileOwner(this, file, name); |
| } catch (IOException e) { |
| handleUserPrincipalLookupServiceException(UserPrincipal.class, name.toString(), e); |
| return null; |
| } |
| } |
| |
| protected void handleUserPrincipalLookupServiceException( |
| Class<? extends Principal> principalType, String name, IOException e) |
| throws IOException { |
| if (log.isTraceEnabled()) { |
| log.trace("handleUserPrincipalLookupServiceException({})[{}] details", principalType.getSimpleName(), name, e); |
| } |
| |
| /* |
| * According to Javadoc: |
| * |
| * "Where an implementation does not support any notion of group or user then this method always throws |
| * UserPrincipalNotFoundException." |
| */ |
| UnsupportedAttributePolicy policy = getUnsupportedAttributePolicy(); |
| switch (policy) { |
| case Ignore: |
| break; |
| case Warn: |
| log.warn("handleUserPrincipalLookupServiceException({})[{}] failed: {}", principalType.getSimpleName(), name, |
| e.toString()); |
| break; |
| case ThrowException: |
| throw e; |
| default: |
| log.warn("Unknown policy ''{}'' for principal={} [{}]", policy, principalType.getSimpleName(), name); |
| } |
| } |
| |
| protected Map<String, Object> readAttrs(Buffer buffer) throws IOException { |
| return SftpHelper.readAttrs(buffer, getVersion()); |
| } |
| |
| /** |
| * Makes sure that the local handle is not null and of the specified type |
| * |
| * @param <H> The generic handle type |
| * @param handle The original handle id |
| * @param h The resolved {@link Handle} instance |
| * @param type The expected handle type |
| * @return The cast type |
| * @throws IOException If a generic exception occurred |
| * @throws FileNotFoundException If the handle instance is {@code null} |
| * @throws InvalidHandleException If the handle instance is not of the expected type |
| */ |
| protected <H extends Handle> H validateHandle(String handle, Handle h, Class<H> type) throws IOException { |
| if (h == null) { |
| throw new NoSuchFileException(handle, handle, "No such current handle"); |
| } |
| |
| Class<?> t = h.getClass(); |
| if (!type.isAssignableFrom(t)) { |
| throw new InvalidHandleException(handle, h, type); |
| } |
| |
| return type.cast(h); |
| } |
| |
| /** |
| * Invoked when an exception was thrown due to the execution of some SFTP command |
| * |
| * @param buffer A {@link Buffer} to be used to build the status reply |
| * @param id Command identifier |
| * @param e Thrown exception |
| * @param cmd The command that was attempted |
| * @param args The relevant command arguments - <B>Note:</B> provided only for <U>logging</U> purposes and |
| * subject to type and/or order change at any version |
| * @throws IOException If failed to build and send the status buffer |
| */ |
| protected void sendStatus(Buffer buffer, int id, Throwable e, int cmd, Object... args) |
| throws IOException { |
| SftpErrorStatusDataHandler handler = getErrorStatusDataHandler(); |
| int subStatus = handler.resolveSubStatus(this, id, e, cmd, args); |
| String message = handler.resolveErrorMessage(this, id, e, subStatus, cmd, args); |
| String lang = handler.resolveErrorLanguage(this, id, e, subStatus, cmd, args); |
| sendStatus(buffer, id, subStatus, message, lang); |
| } |
| |
| protected void sendStatus(Buffer buffer, int id, int substatus, String msg) throws IOException { |
| sendStatus(buffer, id, substatus, (msg != null) ? msg : "", ""); |
| } |
| |
| protected void sendStatus( |
| Buffer buffer, int id, int substatus, String msg, String lang) |
| throws IOException { |
| if (log.isDebugEnabled()) { |
| log.debug("doSendStatus({})[id={}] SSH_FXP_STATUS (substatus={}, lang={}, msg={})", |
| getServerSession(), id, SftpConstants.getStatusName(substatus), lang, msg); |
| } |
| |
| buffer.putByte((byte) SftpConstants.SSH_FXP_STATUS); |
| buffer.putInt(id); |
| buffer.putInt(substatus); |
| buffer.putString(msg); |
| buffer.putString(lang); |
| send(buffer); |
| } |
| |
| protected abstract Buffer prepareReply(Buffer buffer); |
| |
| protected abstract void send(Buffer buffer) throws IOException; |
| |
| protected Path resolveNormalizedLocation(String remotePath) |
| throws IOException, InvalidPathException { |
| Path resolvedPath = resolveFile(remotePath); |
| return normalize(resolvedPath); |
| } |
| |
| protected Path normalize(Path f) { |
| if (f == null) { |
| return null; |
| } |
| |
| Path abs = f.isAbsolute() ? f : f.toAbsolutePath(); |
| return abs.normalize(); |
| } |
| |
| /** |
| * @param remotePath The remote path - separated by '/' |
| * @return The local {@link Path} |
| * @throws IOException If failed to resolve the local path |
| * @throws InvalidPathException If bad local path specification |
| */ |
| protected Path resolveFile(String remotePath) |
| throws IOException, InvalidPathException { |
| SftpFileSystemAccessor accessor = getFileSystemAccessor(); |
| Path localPath = accessor.resolveLocalFilePath(this, getDefaultDirectory(), remotePath); |
| if (log.isTraceEnabled()) { |
| log.trace("resolveFile({}) {} => {}", getServerSession(), remotePath, localPath); |
| } |
| return localPath; |
| } |
| } |