/** * @param props The {@link Properties} holding the server's configuration - ignored if {@code * null}/empty * @param options The {@link LinkOption}s to use when checking files existence * @return A {@link Map} of the found identities where key=the identity type (case * <U>insensitive</U>) and value=the {@link Path} of the file holding the specific type key * @throws IOException If failed to access the file system * @see #getIdentityType(String) * @see #HOST_KEY_CONFIG_PROP * @see org.apache.sshd.common.config.SshConfigFileReader#readConfigFile(File) */ public static Map<String, Path> findIdentities(Properties props, LinkOption... options) throws IOException { if (GenericUtils.isEmpty(props)) { return Collections.emptyMap(); } String keyList = props.getProperty(HOST_KEY_CONFIG_PROP); String[] paths = GenericUtils.split(keyList, ','); if (GenericUtils.isEmpty(paths)) { return Collections.emptyMap(); } Map<String, Path> ids = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (String p : paths) { File file = new File(p); Path path = file.toPath(); if (!Files.exists(path, options)) { continue; } String type = getIdentityType(path.getFileName().toString()); if (GenericUtils.isEmpty(type)) { type = p; // just in case the file name does not adhere to the standard naming convention } Path prev = ids.put(type, path); ValidateUtils.checkTrue(prev == null, "Multiple mappings for type=%s", type); } return ids; }
@Override public void download(String[] remote, Path local, Option... options) throws IOException { download( remote, local, GenericUtils.isEmpty(options) ? Collections.<Option>emptySet() : GenericUtils.of(options)); }
@Override public void upload(Path[] local, String remote, Option... options) throws IOException { upload( local, remote, GenericUtils.isEmpty(options) ? Collections.<Option>emptySet() : GenericUtils.of(options)); }
@Override public void upload(Path local, String remote, Collection<Option> options) throws IOException { upload( new Path[] {ValidateUtils.checkNotNull(local, "Invalid local argument: %s", local)}, remote, GenericUtils.isEmpty(options) ? Collections.<Option>emptySet() : GenericUtils.of(options)); }
@Override public T getPath(String first, String... more) { StringBuilder sb = new StringBuilder(); if (!GenericUtils.isEmpty(first)) { appendDedupSep(sb, first.replace('\\', '/')); // in case we are running on Windows } if (GenericUtils.length(more) > 0) { for (String segment : more) { if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != '/')) { sb.append('/'); } // in case we are running on Windows appendDedupSep(sb, segment.replace('\\', '/')); } } if ((sb.length() > 1) && (sb.charAt(sb.length() - 1) == '/')) { sb.setLength(sb.length() - 1); } String path = sb.toString(); String root = null; if (path.startsWith("/")) { root = "/"; path = path.substring(1); } String[] names = GenericUtils.split(path, '/'); return create(root, names); }
protected Collection<Option> addTargetIsDirectory(Collection<Option> options) { if (GenericUtils.isEmpty(options) || (!options.contains(Option.TargetIsDirectory))) { // create a copy in case the original collection is un-modifiable options = GenericUtils.isEmpty(options) ? EnumSet.noneOf(Option.class) : GenericUtils.of(options); options.add(Option.TargetIsDirectory); } return options; }
protected <T> void setOption( NetworkChannel socket, String property, SocketOption<T> option, T defaultValue) throws IOException { PropertyResolver manager = getFactoryManager(); String valStr = PropertyResolverUtils.getString(manager, property); T val = defaultValue; if (!GenericUtils.isEmpty(valStr)) { Class<T> type = option.type(); if (type == Integer.class) { val = type.cast(Integer.valueOf(valStr)); } else if (type == Boolean.class) { val = type.cast(Boolean.valueOf(valStr)); } else { throw new IllegalStateException("Unsupported socket option type " + type); } } if (val != null) { Collection<? extends SocketOption<?>> supported = socket.supportedOptions(); if ((GenericUtils.size(supported) <= 0) || (!supported.contains(option))) { log.warn( "Unsupported socket option (" + option + ") to set using property '" + property + "' value=" + val); return; } try { socket.setOption(option, val); if (log.isDebugEnabled()) { log.debug("setOption({})[{}] from property={}", option, val, property); } } catch (IOException | RuntimeException e) { log.warn( "Unable (" + e.getClass().getSimpleName() + ")" + " to set socket option " + option + " using property '" + property + "' value=" + val + ": " + e.getMessage()); } } }
@Test public void testLoadClientIdentities() throws Exception { Assume.assumeTrue("BouncyCastle not registered", SecurityUtils.isBouncyCastleRegistered()); Path resFolder = getClassResourcesFolder(TEST_SUBFOLDER, getClass()).toPath(); LinkOption[] options = IoUtils.getLinkOptions(false); Collection<BuiltinIdentities> expected = EnumSet.noneOf(BuiltinIdentities.class); for (BuiltinIdentities type : BuiltinIdentities.VALUES) { String fileName = ClientIdentity.getIdentityFileName(type); Path file = resFolder.resolve(fileName); if (!Files.exists(file, options)) { System.out.println("Skip non-existing identity file " + file); continue; } if (!type.isSupported()) { System.out.println("Skip unsupported identity file " + file); continue; } expected.add(type); } Map<String, KeyPair> ids = ClientIdentity.loadDefaultIdentities( resFolder, false, // don't be strict null, // none of the files is password protected options); assertEquals( "Mismatched loaded ids count", GenericUtils.size(expected), GenericUtils.size(ids)); Collection<KeyPair> pairs = new ArrayList<KeyPair>(ids.size()); for (BuiltinIdentities type : BuiltinIdentities.VALUES) { if (expected.contains(type)) { KeyPair kp = ids.get(type.getName()); assertNotNull("No key pair loaded for " + type, kp); pairs.add(kp); } } KeyPairProvider provider = IdentityUtils.createKeyPairProvider(ids, true /* supported only */); assertNotNull("No provider generated", provider); Iterable<KeyPair> keys = provider.loadKeys(); for (KeyPair kp : keys) { assertTrue("Unexpected loaded key: " + kp, pairs.remove(kp)); } assertEquals("Not all pairs listed", 0, pairs.size()); }
/** * Reads configuration entries * * @param rdr The {@link BufferedReader} to use * @return The {@link List} of read {@link KnownHostEntry}-ies * @throws IOException If failed to parse the read configuration */ public static List<KnownHostEntry> readKnownHostEntries(BufferedReader rdr) throws IOException { List<KnownHostEntry> entries = null; int lineNumber = 1; for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) { line = GenericUtils.trimToEmpty(line); if (GenericUtils.isEmpty(line)) { continue; } int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR); if (pos == 0) { continue; } if (pos > 0) { line = line.substring(0, pos); line = line.trim(); } try { KnownHostEntry entry = parseKnownHostEntry(line); if (entry == null) { continue; } if (entries == null) { entries = new ArrayList<>(); } entries.add(entry); } catch (RuntimeException | Error e) { // TODO consider consulting a user callback throw new StreamCorruptedException( "Failed (" + e.getClass().getSimpleName() + ")" + " to parse line #" + lineNumber + " '" + line + "': " + e.getMessage()); } } if (entries == null) { return Collections.emptyList(); } else { return entries; } }
public Collection<KeyPair> loadKeyPairs( String resourceKey, String pubData, String prvData, String prvEncryption, FilePasswordProvider passwordProvider) throws IOException, GeneralSecurityException { Decoder b64Decoder = Base64.getDecoder(); byte[] pubBytes = b64Decoder.decode(pubData); byte[] prvBytes = b64Decoder.decode(prvData); String password = null; if ((GenericUtils.length(prvEncryption) > 0) && (!NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption))) { password = passwordProvider.getPassword(resourceKey); } if (GenericUtils.isEmpty(prvEncryption) || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption) || GenericUtils.isEmpty(password)) { return loadKeyPairs(resourceKey, pubBytes, prvBytes); } // format is "<cipher><bits>-<mode>" - e.g., "aes256-cbc" int pos = prvEncryption.indexOf('-'); if (pos <= 0) { throw new StreamCorruptedException("Missing private key encryption mode in " + prvEncryption); } String mode = prvEncryption.substring(pos + 1).toUpperCase(); String algName = null; int numBits = 0; for (int index = 0; index < pos; index++) { char ch = prvEncryption.charAt(index); if ((ch >= '0') && (ch <= '9')) { algName = prvEncryption.substring(0, index).toUpperCase(); numBits = Integer.parseInt(prvEncryption.substring(index, pos)); break; } } if (GenericUtils.isEmpty(algName) || (numBits <= 0)) { throw new StreamCorruptedException( "Missing private key encryption algorithm details in " + prvEncryption); } prvBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes( prvBytes, algName, numBits, mode, password); return loadKeyPairs(resourceKey, pubBytes, prvBytes); }
private void closeImmediately0() { // We need to close the channel immediately to remove it from the // server session's channel table and *not* send a packet to the // client. A notification was already sent by our caller, or will // be sent after we return. // super.close(true); // We also need to close the socket. Socket.close(handle); try { if ((forwarder != null) && (!forwarder.isDone())) { forwarder.cancel(true); } } finally { forwarder = null; } try { if ((forwardService != null) && shutdownForwarder) { Collection<?> runners = forwardService.shutdownNow(); if (log.isDebugEnabled()) { log.debug("Shut down runners count=" + GenericUtils.size(runners)); } } } finally { forwardService = null; shutdownForwarder = false; } }
/** * @param <E> The generic entry type * @param entry The {@link PublicKeyEntry} whose contents are to be updated - ignored if {@code * null} * @param data Assumed to contain at least {@code key-type base64-data} (anything beyond the * BASE64 data is ignored) - ignored if {@code null}/empty * @return The updated entry instance * @throws IllegalArgumentException if bad format found */ public static final <E extends PublicKeyEntry> E parsePublicKeyEntry(E entry, String data) throws IllegalArgumentException { if (GenericUtils.isEmpty(data) || (entry == null)) { return entry; } int startPos = data.indexOf(' '); if (startPos <= 0) { throw new IllegalArgumentException("Bad format (no key data delimiter): " + data); } int endPos = data.indexOf(' ', startPos + 1); if (endPos <= startPos) { // OK if no continuation beyond the BASE64 encoded data endPos = data.length(); } String keyType = data.substring(0, startPos); String b64Data = data.substring(startPos + 1, endPos).trim(); byte[] keyData = Base64.decodeString(b64Data); if (NumberUtils.isEmpty(keyData)) { throw new IllegalArgumentException("Bad format (no BASE64 key data): " + data); } entry.setKeyType(keyType); entry.setKeyData(keyData); return entry; }
/** * @param <R> The generic resource type * @param name Name of the resource - ignored if {@code null}/empty * @param c The {@link Comparator} to decide whether the {@link NamedResource#getName()} matches * the <tt>name</tt> parameter * @param resources The {@link NamedResource} to check - ignored if {@code null}/empty * @return The <U>first</U> resource whose name matches the parameter (by invoking {@link * Comparator#compare(Object, Object)} - {@code null} if no match found */ public static <R extends NamedResource> R findByName( String name, Comparator<? super String> c, Collection<? extends R> resources) { if (GenericUtils.isEmpty(name) || GenericUtils.isEmpty(resources)) { return null; } for (R r : resources) { String n = r.getName(); int nRes = c.compare(name, n); if (nRes == 0) { return r; } } return null; }
public SshdSocketAddress(String hostName, int port) { ValidateUtils.checkNotNull(hostName, "Host name may not be null"); this.hostName = GenericUtils.isEmpty(hostName) ? "0.0.0.0" : hostName; ValidateUtils.checkTrue(port >= 0, "Port must be >= 0", Integer.valueOf(port)); this.port = port; }
public static List<String> extractDataLines( String resourceKey, List<String> lines, int startIndex, String hdrName, String hdrValue, List<String> curLines) throws IOException { if (GenericUtils.size(curLines) > 0) { throw new StreamCorruptedException("Duplicate " + hdrName + " in " + resourceKey); } int numLines; try { numLines = Integer.parseInt(hdrValue); } catch (NumberFormatException e) { throw new StreamCorruptedException( "Bad " + hdrName + " value (" + hdrValue + ") in " + resourceKey); } int endIndex = startIndex + numLines; int totalLines = lines.size(); if (endIndex > totalLines) { throw new StreamCorruptedException( "Excessive " + hdrName + " value (" + hdrValue + ") in " + resourceKey); } return lines.subList(startIndex, endIndex); }
@Override public void handleOpenSuccess(int recipient, int rwSize, int packetSize, Buffer buffer) { setRecipient(recipient); Session session = getSession(); FactoryManager manager = ValidateUtils.checkNotNull(session.getFactoryManager(), "No factory manager"); this.remoteWindow.init(rwSize, packetSize, manager.getProperties()); ChannelListener listener = getChannelListenerProxy(); try { doOpen(); listener.channelOpenSuccess(this); this.opened.set(true); this.openFuture.setOpened(); } catch (Throwable t) { Throwable e = GenericUtils.peelException(t); try { listener.channelOpenFailure(this, e); } catch (Throwable ignored) { log.warn( "handleOpenSuccess({}) failed ({}) to inform listener of open failure={}: {}", this, ignored.getClass().getSimpleName(), e.getClass().getSimpleName(), ignored.getMessage()); } this.openFuture.setException(e); this.closeFuture.setClosed(); this.doCloseImmediately(); } finally { notifyStateChanged(); } }
private static CharSequence trimToLength(CharSequence csq, int maxLen) { if (GenericUtils.length(csq) <= maxLen) { return csq; } return csq.subSequence(0, maxLen) + "..."; }
/** * @param data Assumed to contain at least {@code key-type base64-data} (anything beyond the * BASE64 data is ignored) - ignored if {@code null}/empty * @return A {@link PublicKeyEntry} or {@code null} if no data * @throws IllegalArgumentException if bad format found * @see #parsePublicKeyEntry(PublicKeyEntry, String) */ public static final PublicKeyEntry parsePublicKeyEntry(String data) throws IllegalArgumentException { if (GenericUtils.isEmpty(data)) { return null; } else { return parsePublicKeyEntry(new PublicKeyEntry(), data); } }
/** * @param extraSize Extra size - besides the extension name * @return A {@link Buffer} with the extension name set */ protected Buffer getCommandBuffer(int extraSize) { String opcode = getName(); Buffer buffer = new ByteArrayBuffer( (Integer.SIZE / Byte.SIZE) + GenericUtils.length(opcode) + extraSize + Byte.SIZE); buffer.putString(opcode); return buffer; }
/** * Unregisters specified extension * * @param name The factory name - ignored if {@code null}/empty * @return The registered extension - {@code null} if not found */ public static SignatureFactory unregisterExtension(String name) { if (GenericUtils.isEmpty(name)) { return null; } synchronized (EXTENSIONS) { return EXTENSIONS.remove(name); } }
@Override protected OpenFuture doInit(Buffer buffer) { OpenFuture f = new DefaultOpenFuture(this); try { out = new ChannelOutputStream( this, getRemoteWindow(), log, SshConstants.SSH_MSG_CHANNEL_DATA, true); authSocket = PropertyResolverUtils.getString(this, SshAgent.SSH_AUTHSOCKET_ENV_NAME); pool = Pool.create(AprLibrary.getInstance().getRootPool()); handle = Local.create(authSocket, pool); int result = Local.connect(handle, 0); if (result != Status.APR_SUCCESS) { throwException(result); } ExecutorService service = getExecutorService(); forwardService = (service == null) ? ThreadUtils.newSingleThreadExecutor("ChannelAgentForwarding[" + authSocket + "]") : service; shutdownForwarder = service != forwardService || isShutdownOnExit(); final int copyBufSize = PropertyResolverUtils.getIntProperty( this, FORWARDER_BUFFER_SIZE, DEFAULT_FORWARDER_BUF_SIZE); ValidateUtils.checkTrue( copyBufSize >= MIN_FORWARDER_BUF_SIZE, "Copy buf size below min.: %d", copyBufSize); ValidateUtils.checkTrue( copyBufSize <= MAX_FORWARDER_BUF_SIZE, "Copy buf size above max.: %d", copyBufSize); forwarder = forwardService.submit( () -> { try { byte[] buf = new byte[copyBufSize]; while (true) { int len = Socket.recv(handle, buf, 0, buf.length); if (len > 0) { out.write(buf, 0, len); out.flush(); } } } catch (IOException e) { close(true); } }); signalChannelOpenSuccess(); f.setOpened(); } catch (Throwable t) { Throwable e = GenericUtils.peelException(t); signalChannelOpenFailure(e); f.setException(e); } return f; }
public static <E extends KnownHostEntry> E parseKnownHostEntry(E entry, String data) { String line = data; if (GenericUtils.isEmpty(line) || (line.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) { return entry; } entry.setConfigLine(line); if (line.charAt(0) == MARKER_INDICATOR) { int pos = line.indexOf(' '); ValidateUtils.checkTrue(pos > 0, "Missing marker name end delimiter in line=%s", data); ValidateUtils.checkTrue(pos > 1, "No marker name after indicator in line=%s", data); entry.setMarker(line.substring(1, pos)); line = line.substring(pos + 1).trim(); } else { entry.setMarker(null); } int pos = line.indexOf(' '); ValidateUtils.checkTrue(pos > 0, "Missing host patterns end delimiter in line=%s", data); String hostPattern = line.substring(0, pos); line = line.substring(pos + 1).trim(); if (hostPattern.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) { KnownHostHashValue hash = ValidateUtils.checkNotNull( KnownHostHashValue.parse(hostPattern), "Failed to extract host hash value from line=%s", data); entry.setHashedEntry(hash); entry.setPatterns(null); } else { entry.setHashedEntry(null); entry.setPatterns(parsePatterns(GenericUtils.split(hostPattern, ','))); } AuthorizedKeyEntry key = ValidateUtils.checkNotNull( AuthorizedKeyEntry.parseAuthorizedKeyEntry(line), "No valid key entry recovered from line=%s", data); entry.setKeyEntry(key); return entry; }
@Override public SftpSubsystemFactory build() { SftpSubsystemFactory factory = new SftpSubsystemFactory(); factory.setExecutorService(executors); factory.setShutdownOnExit(shutdownExecutor); factory.setUnsupportedAttributePolicy(policy); factory.setFileSystemAccessor(fileSystemAccessor); GenericUtils.forEach(getRegisteredListeners(), factory::addSftpEventListener); return factory; }
/** * @param name The file name - ignored if {@code null}/empty * @return The identity type - {@code null} if cannot determine it - e.g., does not start/end with * the {@link #ID_FILE_PREFIX}/{@link #ID_FILE_SUFFIX} */ public static String getIdentityType(String name) { if (GenericUtils.isEmpty(name) || (name.length() <= (ID_FILE_PREFIX.length() + ID_FILE_SUFFIX.length())) || (!name.startsWith(ID_FILE_PREFIX)) || (!name.endsWith(ID_FILE_SUFFIX))) { return null; } else { return name.substring(ID_FILE_PREFIX.length(), name.length() - ID_FILE_SUFFIX.length()); } }
@Override public Command create() { SftpSubsystem subsystem = new SftpSubsystem( getExecutorService(), isShutdownOnExit(), getUnsupportedAttributePolicy(), getFileSystemAccessor()); GenericUtils.forEach(getRegisteredListeners(), subsystem::addSftpEventListener); return subsystem; }
/** * Checks if a path has strict permissions * * <UL> * <LI> * <p>(For {@code Unix}) The path may not have group or others write permissions * <LI> * <p>The path must be owned by current user. * <LI> * <p>(For {@code Unix}) The path may be owned by root. * </UL> * * @param path The {@link Path} to be checked - ignored if {@code null} or does not exist * @param options The {@link LinkOption}s to use to query the file's permissions * @return The violated permission as {@link Pair} where {@link Pair#getClass()} is a loggable * message and {@link Pair#getSecond()} is the offending object - e.g., {@link * PosixFilePermission} or {@link String} for owner. Return value is {@code null} if no * violations detected * @throws IOException If failed to retrieve the permissions * @see #STRICTLY_PROHIBITED_FILE_PERMISSION */ public static Pair<String, Object> validateStrictConfigFilePermissions( Path path, LinkOption... options) throws IOException { if ((path == null) || (!Files.exists(path, options))) { return null; } Collection<PosixFilePermission> perms = IoUtils.getPermissions(path, options); if (GenericUtils.isEmpty(perms)) { return null; } if (OsUtils.isUNIX()) { PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, STRICTLY_PROHIBITED_FILE_PERMISSION); if (p != null) { return new Pair<String, Object>(String.format("Permissions violation (%s)", p), p); } } String owner = IoUtils.getFileOwner(path, options); if (GenericUtils.isEmpty(owner)) { // we cannot get owner // general issue: jvm does not support permissions // security issue: specific filesystem does not support permissions return null; } String current = OsUtils.getCurrentUser(); Set<String> expected = new HashSet<>(); expected.add(current); if (OsUtils.isUNIX()) { // Windows "Administrator" was considered however in Windows most likely a group is used. expected.add(OsUtils.ROOT_USER); } if (!expected.contains(owner)) { return new Pair<String, Object>(String.format("Owner violation (%s)", owner), owner); } return null; }
/** * @param resources The named resources * @return A {@link List} of all the factories names - in same order as they appear in the input * collection */ public static List<String> getNameList(Collection<? extends NamedResource> resources) { if (GenericUtils.isEmpty(resources)) { return Collections.emptyList(); } List<String> names = new ArrayList<>(resources.size()); for (NamedResource r : resources) { names.add(r.getName()); } return names; }
/** * @param s The {@link Enum}'s name - ignored if {@code null}/empty * @return The matching {@link org.apache.sshd.common.signature.BuiltinSignatures} whose {@link * Enum#name()} matches (case <U>insensitive</U>) the provided argument - {@code null} if no * match */ public static BuiltinSignatures fromString(String s) { if (GenericUtils.isEmpty(s)) { return null; } for (BuiltinSignatures c : VALUES) { if (s.equalsIgnoreCase(c.name())) { return c; } } return null; }
/** * @param name The result name - ignored if {@code null}/empty * @return The matching {@link Result} value (case <U>insensitive</U>) or {@code null} if no * match found */ public static Result fromName(String name) { if (GenericUtils.isEmpty(name)) { return null; } for (Result r : VALUES) { if (name.equalsIgnoreCase(r.name())) { return r; } } return null; }
/** * @param address The request bind address * @param handlerFactory A {@link Factory} to create an {@link IoHandler} if necessary * @return The {@link InetSocketAddress} to which the binding occurred * @throws IOException If failed to bind */ private InetSocketAddress doBind( SshdSocketAddress address, Factory<? extends IoHandler> handlerFactory) throws IOException { if (acceptor == null) { FactoryManager manager = session.getFactoryManager(); IoServiceFactory factory = manager.getIoServiceFactory(); IoHandler handler = handlerFactory.create(); acceptor = factory.createAcceptor(handler); } // TODO find a better way to determine the resulting bind address - what if multi-threaded // calls... Set<SocketAddress> before = acceptor.getBoundAddresses(); try { InetSocketAddress bindAddress = address.toInetSocketAddress(); acceptor.bind(bindAddress); Set<SocketAddress> after = acceptor.getBoundAddresses(); if (GenericUtils.size(after) > 0) { after.removeAll(before); } if (GenericUtils.isEmpty(after)) { throw new IOException( "Error binding to " + address + "[" + bindAddress + "]: no local addresses bound"); } if (after.size() > 1) { throw new IOException( "Multiple local addresses have been bound for " + address + "[" + bindAddress + "]"); } return (InetSocketAddress) after.iterator().next(); } catch (IOException bindErr) { Set<SocketAddress> after = acceptor.getBoundAddresses(); if (GenericUtils.isEmpty(after)) { close(); } throw bindErr; } }