http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestUtils.java new file mode 100644 index 0000000..30369ac --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/DigestUtils.java @@ -0,0 +1,228 @@ +/* + * 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.common.digest; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; + +import org.apache.sshd.common.Factory; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class DigestUtils { + private DigestUtils() { + throw new UnsupportedOperationException("No instance"); + } + + /** + * @param algorithm The digest algorithm - never {@code null}/empty + * @return {@code true} if this digest algorithm is supported + * @see SecurityUtils#getMessageDigest(String) + */ + public static boolean checkSupported(String algorithm) { + ValidateUtils.checkNotNullAndNotEmpty(algorithm, "No algorithm"); + try { + MessageDigest digest = SecurityUtils.getMessageDigest(algorithm); + return digest != null; // just in case + } catch (Exception e) { + return false; + } + } + + /** + * @param <D> The generic type of digest factory + * @param algo The required algorithm name - ignored if {@code null}/empty + * @param comp The {@link Comparator} to use to compare algorithm names + * @param digests The factories to check - ignored if {@code null}/empty + * @return The first {@link DigestFactory} whose algorithm matches the required one + * according to the comparator - {@code null} if no match found + */ + public static <D extends Digest> D findDigestByAlgorithm(String algo, Comparator<? super String> comp, Collection<? extends D> digests) { + if (GenericUtils.isEmpty(algo) || GenericUtils.isEmpty(digests)) { + return null; + } + + for (D d : digests) { + if (comp.compare(algo, d.getAlgorithm()) == 0) { + return d; + } + } + + return null; + } + + /** + * @param <F> The generic type of digest factory + * @param algo The required algorithm name - ignored if {@code null}/empty + * @param comp The {@link Comparator} to use to compare algorithm names + * @param factories The factories to check - ignored if {@code null}/empty + * @return The first {@link DigestFactory} whose algorithm matches the required one + * according to the comparator - {@code null} if no match found + */ + public static <F extends DigestFactory> F findFactoryByAlgorithm(String algo, Comparator<? super String> comp, Collection<? extends F> factories) { + if (GenericUtils.isEmpty(algo) || GenericUtils.isEmpty(factories)) { + return null; + } + + for (F f : factories) { + if (comp.compare(algo, f.getAlgorithm()) == 0) { + return f; + } + } + + return null; + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty, + * otherwise its UTF-8 representation is used as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input + * @throws Exception If failed to calculate the digest + * @see #getFingerPrint(Digest, String, Charset) + */ + public static String getFingerPrint(Factory<? extends Digest> f, String s) throws Exception { + return getFingerPrint(f, s, StandardCharsets.UTF_8); + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty + * @param charset The {@link Charset} to use in order to convert the + * string to its byte representation to use as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input + * @throws Exception If failed to calculate the digest + */ + public static String getFingerPrint(Factory<? extends Digest> f, String s, Charset charset) throws Exception { + return getFingerPrint(Objects.requireNonNull(f, "No factory").create(), s, charset); + } + + /** + * @param d The {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty, + * otherwise its UTF-8 representation is used as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input + * @throws Exception If failed to calculate the digest + * @see #getFingerPrint(Digest, String, Charset) + */ + public static String getFingerPrint(Digest d, String s) throws Exception { + return getFingerPrint(d, s, StandardCharsets.UTF_8); + } + + /** + * @param d The {@link Digest} to use + * @param s The {@link String} to digest - ignored if {@code null}/empty + * @param charset The {@link Charset} to use in order to convert the + * string to its byte representation to use as input for the fingerprint + * @return The fingerprint - {@code null} if {@code null}/empty input + * @throws Exception If failed to calculate the digest + */ + public static String getFingerPrint(Digest d, String s, Charset charset) throws Exception { + if (GenericUtils.isEmpty(s)) { + return null; + } else { + return DigestUtils.getFingerPrint(d, s.getBytes(charset)); + } + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param buf The data buffer to be fingerprint-ed + * @return The fingerprint - {@code null} if empty data buffer + * @throws Exception If failed to calculate the fingerprint + * @see #getFingerPrint(Factory, byte[], int, int) + */ + public static String getFingerPrint(Factory<? extends Digest> f, byte... buf) throws Exception { + return getFingerPrint(f, buf, 0, NumberUtils.length(buf)); + } + + /** + * @param f The {@link Factory} to create the {@link Digest} to use + * @param buf The data buffer to be fingerprint-ed + * @param offset The offset of the data in the buffer + * @param len The length of data - ignored if non-positive + * @return The fingerprint - {@code null} if non-positive length + * @throws Exception If failed to calculate the fingerprint + */ + public static String getFingerPrint(Factory<? extends Digest> f, byte[] buf, int offset, int len) throws Exception { + return getFingerPrint(Objects.requireNonNull(f, "No factory").create(), buf, offset, len); + } + + /** + * @param d The {@link Digest} to use + * @param buf The data buffer to be fingerprint-ed + * @return The fingerprint - {@code null} if empty data buffer + * @throws Exception If failed to calculate the fingerprint + * @see #getFingerPrint(Digest, byte[], int, int) + */ + public static String getFingerPrint(Digest d, byte... buf) throws Exception { + return getFingerPrint(d, buf, 0, NumberUtils.length(buf)); + } + + /** + * @param d The {@link Digest} to use + * @param buf The data buffer to be fingerprint-ed + * @param offset The offset of the data in the buffer + * @param len The length of data - ignored if non-positive + * @return The fingerprint - {@code null} if non-positive length + * @throws Exception If failed to calculate the fingerprint + * @see #getRawFingerprint(Digest, byte[], int, int) + */ + public static String getFingerPrint(Digest d, byte[] buf, int offset, int len) throws Exception { + if (len <= 0) { + return null; + } + + byte[] data = getRawFingerprint(d, buf, offset, len); + String algo = d.getAlgorithm(); + if (BuiltinDigests.md5.getAlgorithm().equals(algo)) { + return algo + ":" + BufferUtils.toHex(':', data).toLowerCase(); + } + + Base64.Encoder encoder = Base64.getEncoder(); + return algo.replace("-", "").toUpperCase() + ":" + encoder.encodeToString(data).replaceAll("=", ""); + } + + public static byte[] getRawFingerprint(Digest d, byte... buf) throws Exception { + return getRawFingerprint(d, buf, 0, NumberUtils.length(buf)); + } + + public static byte[] getRawFingerprint(Digest d, byte[] buf, int offset, int len) throws Exception { + if (len <= 0) { + return null; + } + + Objects.requireNonNull(d, "No digest").init(); + d.update(buf, offset, len); + + return d.digest(); + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/digest/package.html ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/digest/package.html b/sshd-common/src/main/java/org/apache/sshd/common/digest/package.html new file mode 100644 index 0000000..65940e0 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/digest/package.html @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<html> +<head> +</head> +<body> + +<a href="{@docRoot}/org/apache/sshd/common/digest/Digest.html"><code>Digest</code></a> implementations. + +</body> +</html> http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/AbstractSshFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/AbstractSshFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/AbstractSshFuture.java new file mode 100644 index 0000000..1dca54c --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/AbstractSshFuture.java @@ -0,0 +1,194 @@ +/* + * 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.common.future; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.StreamCorruptedException; +import java.util.function.Function; + +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * @param <T> Type of future + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractSshFuture<T extends SshFuture> extends AbstractLoggingBean implements SshFuture<T> { + /** + * A default value to indicate the future has been canceled + */ + protected static final Object CANCELED = new Object(); + + protected final boolean debugEnabled; + protected final boolean traceEnabled; + private final Object id; + + /** + * @param id Some identifier useful as {@link #toString()} value + */ + protected AbstractSshFuture(Object id) { + this.id = id; + this.debugEnabled = log.isDebugEnabled(); + this.traceEnabled = log.isTraceEnabled(); + } + + @Override + public Object getId() { + return id; + } + + @Override + public boolean await(long timeoutMillis) throws IOException { + return await0(timeoutMillis, true) != null; + } + + @Override + public boolean awaitUninterruptibly(long timeoutMillis) { + try { + return await0(timeoutMillis, false) != null; + } catch (InterruptedIOException e) { + throw formatExceptionMessage(msg -> new InternalError(msg, e), + "Unexpected interrupted exception wile awaitUninterruptibly %d msec: %s", + timeoutMillis, e.getMessage()); + } + } + + /** + * <P>Waits (interruptible) for the specified timeout (msec.) and then checks + * the result:</P> + * <UL> + * <LI><P> + * If result is {@code null} then timeout is assumed to have expired - throw + * an appropriate {@link IOException} + * </P></LI> + * + * <LI><P> + * If the result is of the expected type, then cast and return it + * </P></LI> + * + * <LI><P> + * If the result is an {@link IOException} then re-throw it + * </P></LI> + * + * <LI><P> + * If the result is a {@link Throwable} then throw an {@link IOException} + * whose cause is the original exception + * </P></LI> + * + * <LI><P> + * Otherwise (should never happen), throw a {@link StreamCorruptedException} + * with the name of the result type + * </P></LI> + * </UL> + * + * @param <R> The generic result type + * @param expectedType The expected result type + * @param timeout The timeout (millis) to wait for a result + * @return The (never {@code null}) result + * @throws IOException If failed to retrieve the expected result on time + */ + protected <R> R verifyResult(Class<? extends R> expectedType, long timeout) throws IOException { + Object value = await0(timeout, true); + if (value == null) { + throw formatExceptionMessage(SshException::new, "Failed to get operation result within specified timeout: %s", timeout); + } + + Class<?> actualType = value.getClass(); + if (expectedType.isAssignableFrom(actualType)) { + return expectedType.cast(value); + } + + if (Throwable.class.isAssignableFrom(actualType)) { + Throwable t = GenericUtils.peelException((Throwable) value); + if (t != value) { + value = t; + actualType = value.getClass(); + } + + if (IOException.class.isAssignableFrom(actualType)) { + throw (IOException) value; + } + + Throwable cause = GenericUtils.resolveExceptionCause(t); + throw formatExceptionMessage(msg -> new SshException(msg, cause), "Failed (%s) to execute: %s", t.getClass().getSimpleName(), t.getMessage()); + } else { // what else can it be ???? + throw formatExceptionMessage(StreamCorruptedException::new, "Unknown result type: %s", actualType.getName()); + } + } + + /** + * Wait for the Future to be ready. If the requested delay is 0 or + * negative, this method returns immediately. + * + * @param timeoutMillis The delay we will wait for the Future to be ready + * @param interruptable Tells if the wait can be interrupted or not. + * If {@code true} and the thread is interrupted then an {@link InterruptedIOException} + * is thrown. + * @return The non-{@code null} result object if the Future is ready, + * {@code null} if the timeout expired and no result was received + * @throws InterruptedIOException If the thread has been interrupted when it's not allowed. + */ + protected abstract Object await0(long timeoutMillis, boolean interruptable) throws InterruptedIOException; + + @SuppressWarnings("unchecked") + protected SshFutureListener<T> asListener(Object o) { + return (SshFutureListener<T>) o; + } + + protected void notifyListener(SshFutureListener<T> l) { + try { + l.operationComplete(asT()); + } catch (Throwable t) { + log.warn("notifyListener({}) failed ({}) to invoke {}: {}", + this, t.getClass().getSimpleName(), l, t.getMessage()); + if (debugEnabled) { + log.debug("notifyListener(" + this + ")[" + l + "] invocation failure details", t); + } + } + } + + @SuppressWarnings("unchecked") + protected T asT() { + return (T) this; + } + + /** + * Generates an exception whose message is prefixed by the future simple class name + {@link #getId() identifier} + * as a hint to the context of the failure. + * + * @param <E> Type of {@link Throwable} being generated + * @param exceptionCreator The exception creator from the formatted message + * @param format The extra payload format as per {@link String#format(String, Object...)} + * @param args The formatting arguments + * @return The generated exception + */ + protected <E extends Throwable> E formatExceptionMessage(Function<? super String, ? extends E> exceptionCreator, String format, Object... args) { + String messagePayload = String.format(format, args); + String excMessage = getClass().getSimpleName() + "[" + getId() + "]: " + messagePayload; + return exceptionCreator.apply(excMessage); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[id=" + getId() + "]"; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/CloseFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/CloseFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/CloseFuture.java new file mode 100644 index 0000000..808716d --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/CloseFuture.java @@ -0,0 +1,40 @@ +/* + * 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.common.future; + +/** + * An {@link SshFuture} for asynchronous close requests. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface CloseFuture extends SshFuture<CloseFuture> { + + /** + * @return <tt>true</tt> if the close request is finished and the target is closed. + */ + boolean isClosed(); + + /** + * Marks this future as closed and notifies all threads waiting for this + * future. This method is invoked by SSHD internally. Please do not call + * this method directly. + */ + void setClosed(); + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultCloseFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultCloseFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultCloseFuture.java new file mode 100644 index 0000000..4c34a06 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultCloseFuture.java @@ -0,0 +1,52 @@ +/* + * 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.common.future; + +/** + * A default implementation of {@link CloseFuture}. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DefaultCloseFuture extends DefaultSshFuture<CloseFuture> implements CloseFuture { + + /** + * Create a new instance + * + * @param id Some identifier useful as {@link #toString()} value + * @param lock A synchronization object for locking access - if {@code null} + * then synchronization occurs on {@code this} instance + */ + public DefaultCloseFuture(Object id, Object lock) { + super(id, lock); + } + + @Override + public boolean isClosed() { + if (isDone()) { + return (Boolean) getValue(); + } else { + return false; + } + } + + @Override + public void setClosed() { + setValue(Boolean.TRUE); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultSshFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultSshFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultSshFuture.java new file mode 100644 index 0000000..cd475ff --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultSshFuture.java @@ -0,0 +1,236 @@ +/* + * 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.common.future; + +import java.io.InterruptedIOException; +import java.lang.reflect.Array; +import java.util.Objects; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * A default implementation of {@link SshFuture}. + * + * @param <T> Type of future + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DefaultSshFuture<T extends SshFuture> extends AbstractSshFuture<T> { + /** + * A lock used by the wait() method + */ + private final Object lock; + private Object listeners; + private Object result; + + /** + * Creates a new instance. + * + * @param id Some identifier useful as {@link #toString()} value + * @param lock A synchronization object for locking access - if {@code null} + * then synchronization occurs on {@code this} instance + */ + public DefaultSshFuture(Object id, Object lock) { + super(id); + + this.lock = (lock != null) ? lock : this; + } + + @Override + protected Object await0(long timeoutMillis, boolean interruptable) throws InterruptedIOException { + ValidateUtils.checkTrue(timeoutMillis >= 0L, "Negative timeout N/A: %d", timeoutMillis); + long startTime = System.currentTimeMillis(); + long curTime = startTime; + long endTime = ((Long.MAX_VALUE - timeoutMillis) < curTime) ? Long.MAX_VALUE : (curTime + timeoutMillis); + + synchronized (lock) { + if ((result != null) || (timeoutMillis <= 0)) { + return result; + } + + for (;;) { + try { + lock.wait(endTime - curTime); + } catch (InterruptedException e) { + if (interruptable) { + curTime = System.currentTimeMillis(); + throw formatExceptionMessage(msg -> { + InterruptedIOException exc = new InterruptedIOException(msg); + exc.initCause(e); + return exc; + }, "Interrupted after %d msec.", curTime - startTime); + } + } + + curTime = System.currentTimeMillis(); + if ((result != null) || (curTime >= endTime)) { + return result; + } + } + } + } + + @Override + public boolean isDone() { + synchronized (lock) { + return result != null; + } + } + + /** + * Sets the result of the asynchronous operation, and mark it as finished. + * + * @param newValue The operation result + */ + public void setValue(Object newValue) { + synchronized (lock) { + // Allow only once. + if (result != null) { + return; + } + + result = (newValue != null) ? newValue : GenericUtils.NULL; + lock.notifyAll(); + } + + notifyListeners(); + } + + public int getNumRegisteredListeners() { + synchronized (lock) { + if (listeners == null) { + return 0; + } else if (listeners instanceof SshFutureListener) { + return 1; + } else { + int l = Array.getLength(listeners); + int count = 0; + for (int i = 0; i < l; i++) { + if (Array.get(listeners, i) != null) { + count++; + } + } + return count; + } + } + } + + /** + * @return The result of the asynchronous operation - or {@code null} + * if none set. + */ + public Object getValue() { + synchronized (lock) { + return (result == GenericUtils.NULL) ? null : result; + } + } + + @Override + public T addListener(SshFutureListener<T> listener) { + Objects.requireNonNull(listener, "Missing listener argument"); + boolean notifyNow = false; + synchronized (lock) { + // if already have a result don't register the listener and invoke it directly + if (result != null) { + notifyNow = true; + } else if (listeners == null) { + listeners = listener; // 1st listener ? + } else if (listeners instanceof SshFutureListener) { + listeners = new Object[]{listeners, listener}; + } else { // increase array of registered listeners + Object[] ol = (Object[]) listeners; + int l = ol.length; + Object[] nl = new Object[l + 1]; + System.arraycopy(ol, 0, nl, 0, l); + nl[l] = listener; + listeners = nl; + } + } + + if (notifyNow) { + notifyListener(listener); + } + + return asT(); + } + + @Override + public T removeListener(SshFutureListener<T> listener) { + Objects.requireNonNull(listener, "No listener provided"); + + synchronized (lock) { + if (result != null) { + return asT(); // the train has already left the station... + } + + if (listeners == null) { + return asT(); // no registered instances anyway + } + + if (listeners == listener) { + listeners = null; // the one and only + } else if (!(listeners instanceof SshFutureListener)) { + int l = Array.getLength(listeners); + for (int i = 0; i < l; i++) { + if (Array.get(listeners, i) == listener) { + Array.set(listeners, i, null); + break; + } + } + } + } + + return asT(); + } + + protected void notifyListeners() { + /* + * There won't be any visibility problem or concurrent modification + * because result value is checked in both addListener and + * removeListener calls under lock. If the result is already set then + * both methods will not modify the internal listeners + */ + if (listeners != null) { + if (listeners instanceof SshFutureListener) { + notifyListener(asListener(listeners)); + } else { + int l = Array.getLength(listeners); + for (int i = 0; i < l; i++) { + SshFutureListener<T> listener = asListener(Array.get(listeners, i)); + if (listener != null) { + notifyListener(listener); + } + } + } + } + } + + public boolean isCanceled() { + return getValue() == CANCELED; + } + + public void cancel() { + setValue(CANCELED); + } + + @Override + public String toString() { + return super.toString() + "[value=" + result + "]"; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultVerifiableSshFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultVerifiableSshFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultVerifiableSshFuture.java new file mode 100644 index 0000000..1b02fed --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/DefaultVerifiableSshFuture.java @@ -0,0 +1,30 @@ +/* + * 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.common.future; + +/** + * @param <T> Type of future + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class DefaultVerifiableSshFuture<T extends SshFuture> extends DefaultSshFuture<T> implements VerifiableFuture<T> { + protected DefaultVerifiableSshFuture(Object id, Object lock) { + super(id, lock); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/SshFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/SshFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/SshFuture.java new file mode 100644 index 0000000..b0d8e3c --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/SshFuture.java @@ -0,0 +1,48 @@ +/* + * 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.common.future; + +/** + * Represents the completion of an asynchronous SSH operation on a given object + * (it may be an SSH session or an SSH channel). + * Can be listened for completion using a {@link SshFutureListener}. + * + * @param <T> Type of future + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface SshFuture<T extends SshFuture> extends WaitableFuture { + /** + * Adds an event <tt>listener</tt> which is notified when + * this future is completed. If the listener is added + * after the completion, the listener is directly notified. + * + * @param listener The {@link SshFutureListener} instance to add + * @return The future instance + */ + T addListener(SshFutureListener<T> listener); + + /** + * Removes an existing event <tt>listener</tt> so it won't be notified when + * the future is completed. + * + * @param listener The {@link SshFutureListener} instance to remove + * @return The future instance + */ + T removeListener(SshFutureListener<T> listener); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/SshFutureListener.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/SshFutureListener.java b/sshd-common/src/main/java/org/apache/sshd/common/future/SshFutureListener.java new file mode 100644 index 0000000..a005c0f --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/SshFutureListener.java @@ -0,0 +1,42 @@ +/* + * 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.common.future; + +import org.apache.sshd.common.util.SshdEventListener; + +/** + * Something interested in being notified when the completion + * of an asynchronous SSH operation : {@link SshFuture}. + * + * @param <T> type of future + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@SuppressWarnings("rawtypes") +@FunctionalInterface +public interface SshFutureListener<T extends SshFuture> extends SshdEventListener { + + /** + * Invoked when the operation associated with the {@link SshFuture} + * has been completed even if you add the listener after the completion. + * + * @param future The source {@link SshFuture} which called this + * callback. + */ + void operationComplete(T future); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/VerifiableFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/VerifiableFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/VerifiableFuture.java new file mode 100644 index 0000000..e318de7 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/VerifiableFuture.java @@ -0,0 +1,68 @@ +/* + * 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.common.future; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * Represents an asynchronous operation whose successful result can be + * verified somehow. The contract guarantees that if the {@code verifyXXX} + * method returns without an exception then the operation was completed + * <U>successfully</U> + * + * @param <T> Type of verification result + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface VerifiableFuture<T> { + /** + * Wait {@link Long#MAX_VALUE} msec. and verify that the operation was successful + * + * @return The (same) future instance + * @throws IOException If failed to verify successfully on time + * @see #verify(long) + */ + default T verify() throws IOException { + return verify(Long.MAX_VALUE); + } + + /** + * Wait and verify that the operation was successful + * + * @param timeout The number of time units to wait + * @param unit The wait {@link TimeUnit} + * @return The (same) future instance + * @throws IOException If failed to verify successfully on time + * @see #verify(long) + */ + default T verify(long timeout, TimeUnit unit) throws IOException { + return verify(unit.toMillis(timeout)); + } + + /** + * Wait and verify that the operation was successful + * + * @param timeoutMillis Wait timeout in milliseconds + * @return The (same) future instance + * @throws IOException If failed to verify successfully on time + */ + T verify(long timeoutMillis) throws IOException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/future/WaitableFuture.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/future/WaitableFuture.java b/sshd-common/src/main/java/org/apache/sshd/common/future/WaitableFuture.java new file mode 100644 index 0000000..aff4adc --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/future/WaitableFuture.java @@ -0,0 +1,118 @@ +/* + * 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.common.future; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * Represents an asynchronous operation which one can wait for its completion. + * <B>Note:</B> the only thing guaranteed is that if {@code true} is returned + * from one of the {@code awaitXXX} methods then the operation has completed. + * However, the <B>caller</B> has to determine whether it was a successful or + * failed completion. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface WaitableFuture { + /** + * @return Some identifier useful as {@link #toString()} value + */ + Object getId(); + + /** + * Wait {@link Long#MAX_VALUE} msec. for the asynchronous operation to complete. + * The attached listeners will be notified when the operation is + * completed. + * + * @return {@code true} if the operation is completed. + * @throws IOException if failed - specifically {@link java.io.InterruptedIOException} + * if waiting was interrupted + * @see #await(long) + */ + default boolean await() throws IOException { + return await(Long.MAX_VALUE); + } + + /** + * Wait for the asynchronous operation to complete with the specified timeout. + * + * @param timeout The number of time units to wait + * @param unit The {@link TimeUnit} for waiting + * @return {@code true} if the operation is completed. + * @throws IOException if failed - specifically {@link java.io.InterruptedIOException} + * if waiting was interrupted + * @see #await(long) + */ + default boolean await(long timeout, TimeUnit unit) throws IOException { + return await(unit.toMillis(timeout)); + } + + /** + * Wait for the asynchronous operation to complete with the specified timeout. + * + * @param timeoutMillis Wait time in milliseconds + * @return {@code true} if the operation is completed. + * @throws IOException if failed - specifically {@link java.io.InterruptedIOException} + * if waiting was interrupted + */ + boolean await(long timeoutMillis) throws IOException; + + /** + * Wait {@link Long#MAX_VALUE} msec. for the asynchronous operation to complete + * uninterruptibly. The attached listeners will be notified when the operation is + * completed. + * + * @return {@code true} if the operation is completed. + * @see #awaitUninterruptibly(long) + */ + default boolean awaitUninterruptibly() { + return awaitUninterruptibly(Long.MAX_VALUE); + } + + /** + * Wait for the asynchronous operation to complete with the specified timeout + * uninterruptibly. + * + * @param timeout The number of time units to wait + * @param unit The {@link TimeUnit} for waiting + * @return {@code true} if the operation is completed. + * @see #awaitUninterruptibly(long) + */ + default boolean awaitUninterruptibly(long timeout, TimeUnit unit) { + return awaitUninterruptibly(unit.toMillis(timeout)); + } + + /** + * Wait for the asynchronous operation to complete with the specified timeout + * uninterruptibly. + * + * @param timeoutMillis Wait time in milliseconds + * @return {@code true} if the operation is finished. + */ + boolean awaitUninterruptibly(long timeoutMillis); + + /** + * @return {@code true} if the asynchronous operation is completed. <B>Note:</B> + * it is up to the <B>caller</B> to determine whether it was a successful or + * failed completion. + */ + boolean isDone(); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java new file mode 100644 index 0000000..077d199 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java @@ -0,0 +1,32 @@ +/* + * 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.common.keyprovider; + +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * Provides a default implementation for some {@link KeyPairProvider} methods + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractKeyPairProvider extends AbstractLoggingBean implements KeyPairProvider { + protected AbstractKeyPairProvider() { + super(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractResourceKeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractResourceKeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractResourceKeyPairProvider.java new file mode 100644 index 0000000..e4e941d --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractResourceKeyPairProvider.java @@ -0,0 +1,234 @@ +/* + * 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.common.keyprovider; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @param <R> Type of resource from which the {@link KeyPair} is generated + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractResourceKeyPairProvider<R> extends AbstractKeyPairProvider { + private FilePasswordProvider passwordFinder; + /* + * NOTE: the map is case insensitive even for Linux, as it is (very) bad + * practice to have 2 key files that differ from one another only in their + * case... + */ + private final Map<String, KeyPair> cacheMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + protected AbstractResourceKeyPairProvider() { + super(); + } + + public FilePasswordProvider getPasswordFinder() { + return passwordFinder; + } + + public void setPasswordFinder(FilePasswordProvider passwordFinder) { + this.passwordFinder = passwordFinder; + } + + /** + * Checks which of the new resources we already loaded and can keep the + * associated key pair + * + * @param resources The collection of new resources - can be {@code null}/empty + * in which case the cache is cleared + */ + protected void resetCacheMap(Collection<?> resources) { + // if have any cached pairs then see what we can keep from previous load + Collection<String> toDelete = Collections.emptySet(); + synchronized (cacheMap) { + if (cacheMap.size() <= 0) { + return; // already empty - nothing to keep + } + + if (GenericUtils.isEmpty(resources)) { + cacheMap.clear(); + return; + } + + for (Object r : resources) { + String resourceKey = ValidateUtils.checkNotNullAndNotEmpty(Objects.toString(r, null), "No resource key value"); + if (cacheMap.containsKey(resourceKey)) { + continue; + } + + if (toDelete.isEmpty()) { + toDelete = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + } + + if (!toDelete.add(resourceKey)) { + continue; // debug breakpoint + } + } + + if (GenericUtils.size(toDelete) > 0) { + toDelete.forEach(cacheMap::remove); + } + } + + if (log.isDebugEnabled()) { + log.debug("resetCacheMap(" + resources + ") removed previous cached keys for " + toDelete); + } + } + + protected Iterable<KeyPair> loadKeys(final Collection<? extends R> resources) { + if (GenericUtils.isEmpty(resources)) { + return Collections.emptyList(); + } else { + return () -> new KeyPairIterator(resources); + } + } + + protected KeyPair doLoadKey(R resource) throws IOException, GeneralSecurityException { + String resourceKey = ValidateUtils.checkNotNullAndNotEmpty(Objects.toString(resource, null), "No resource string value"); + KeyPair kp; + synchronized (cacheMap) { + // check if lucky enough to have already loaded this file + kp = cacheMap.get(resourceKey); + } + + if (kp != null) { + if (log.isTraceEnabled()) { + PublicKey key = kp.getPublic(); + log.trace("doLoadKey({}) use cached key {}-{}", + resourceKey, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + } + return kp; + } + + kp = doLoadKey(resourceKey, resource, getPasswordFinder()); + if (kp != null) { + boolean reusedKey; + synchronized (cacheMap) { + // if somebody else beat us to it, use the cached key - just in case file contents changed + reusedKey = cacheMap.containsKey(resourceKey); + if (reusedKey) { + kp = cacheMap.get(resourceKey); + } else { + cacheMap.put(resourceKey, kp); + } + } + + if (log.isDebugEnabled()) { + PublicKey key = kp.getPublic(); + log.debug("doLoadKey({}) {} {}-{}", + resourceKey, reusedKey ? "re-loaded" : "loaded", + KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + } + } else { + if (log.isDebugEnabled()) { + log.debug("doLoadKey({}) no key loaded", resourceKey); + } + } + + return kp; + } + + protected KeyPair doLoadKey(String resourceKey, R resource, FilePasswordProvider provider) throws IOException, GeneralSecurityException { + try (InputStream inputStream = openKeyPairResource(resourceKey, resource)) { + return doLoadKey(resourceKey, inputStream, provider); + } + } + + protected abstract InputStream openKeyPairResource(String resourceKey, R resource) throws IOException; + + protected KeyPair doLoadKey(String resourceKey, InputStream inputStream, FilePasswordProvider provider) + throws IOException, GeneralSecurityException { + return SecurityUtils.loadKeyPairIdentity(resourceKey, inputStream, provider); + } + + protected class KeyPairIterator implements Iterator<KeyPair> { + private final Iterator<? extends R> iterator; + private KeyPair nextKeyPair; + private boolean nextKeyPairSet; + + protected KeyPairIterator(Collection<? extends R> resources) { + iterator = resources.iterator(); + } + + @Override + public boolean hasNext() { + return nextKeyPairSet || setNextObject(); + } + + @Override + public KeyPair next() { + if (!nextKeyPairSet) { + if (!setNextObject()) { + throw new NoSuchElementException("Out of files to try"); + } + } + nextKeyPairSet = false; + return nextKeyPair; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("loadKeys(files) Iterator#remove() N/A"); + } + + @SuppressWarnings("synthetic-access") + private boolean setNextObject() { + boolean debugEnabled = log.isDebugEnabled(); + while (iterator.hasNext()) { + R r = iterator.next(); + try { + nextKeyPair = doLoadKey(r); + } catch (Throwable e) { + log.warn("Failed (" + e.getClass().getSimpleName() + ")" + + " to load key resource=" + r + ": " + e.getMessage()); + if (debugEnabled) { + log.debug("Key resource=" + r + " load failure details", e); + } + nextKeyPair = null; + continue; + } + + if (nextKeyPair != null) { + nextKeyPairSet = true; + return true; + } + } + + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/ClassLoadableResourceKeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/ClassLoadableResourceKeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/ClassLoadableResourceKeyPairProvider.java new file mode 100644 index 0000000..6e8da54 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/ClassLoadableResourceKeyPairProvider.java @@ -0,0 +1,113 @@ +/* + * 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.common.keyprovider; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.security.KeyPair; +import java.util.Collection; +import java.util.Collections; + +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.threads.ThreadUtils; + +/** + * This provider loads private keys from the specified resources that + * are accessible via {@link ClassLoader#getResourceAsStream(String)}. + * If no loader configured via {@link #setResourceLoader(ClassLoader)}, then + * {@link ThreadUtils#resolveDefaultClassLoader(Class)} is used + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class ClassLoadableResourceKeyPairProvider extends AbstractResourceKeyPairProvider<String> { + private ClassLoader classLoader; + private Collection<String> resources; + + public ClassLoadableResourceKeyPairProvider() { + this(Collections.emptyList()); + } + + public ClassLoadableResourceKeyPairProvider(ClassLoader cl) { + this(cl, Collections.emptyList()); + } + + public ClassLoadableResourceKeyPairProvider(String res) { + this(Collections.singletonList(ValidateUtils.checkNotNullAndNotEmpty(res, "No resource specified"))); + } + + public ClassLoadableResourceKeyPairProvider(ClassLoader cl, String res) { + this(cl, Collections.singletonList(ValidateUtils.checkNotNullAndNotEmpty(res, "No resource specified"))); + } + + public ClassLoadableResourceKeyPairProvider(Collection<String> resources) { + this.classLoader = ThreadUtils.resolveDefaultClassLoader(getClass()); + this.resources = (resources == null) ? Collections.emptyList() : resources; + } + + public ClassLoadableResourceKeyPairProvider(ClassLoader cl, Collection<String> resources) { + this.classLoader = cl; + this.resources = (resources == null) ? Collections.emptyList() : resources; + } + + public Collection<String> getResources() { + return resources; + } + + public void setResources(Collection<String> resources) { + this.resources = (resources == null) ? Collections.emptyList() : resources; + } + + public ClassLoader getResourceLoader() { + return classLoader; + } + + public void setResourceLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public Iterable<KeyPair> loadKeys() { + return loadKeys(getResources()); + } + + @Override + protected InputStream openKeyPairResource(String resourceKey, String resource) throws IOException { + ClassLoader cl = resolveClassLoader(); + if (cl == null) { + throw new StreamCorruptedException("No resource loader for " + resource); + } + + InputStream input = cl.getResourceAsStream(resource); + if (input == null) { + throw new FileNotFoundException("Cannot find resource " + resource); + } + + return input; + } + + protected ClassLoader resolveClassLoader() { + ClassLoader cl = getResourceLoader(); + if (cl == null) { + cl = ThreadUtils.resolveDefaultClassLoader(getClass()); + } + return cl; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileKeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileKeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileKeyPairProvider.java new file mode 100644 index 0000000..c4aae97 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileKeyPairProvider.java @@ -0,0 +1,92 @@ +/* + * 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.common.keyprovider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.io.IoUtils; + +/** + * This host key provider loads private keys from the specified files. The + * loading is <U>lazy</U> - i.e., a file is not loaded until it is actually + * required. Once required though, its loaded {@link KeyPair} result is + * <U>cached</U> and not re-loaded. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class FileKeyPairProvider extends AbstractResourceKeyPairProvider<Path> { + private Collection<? extends Path> files; + + public FileKeyPairProvider() { + super(); + } + + public FileKeyPairProvider(Path path) { + this(Collections.singletonList(Objects.requireNonNull(path, "No path provided"))); + } + + public FileKeyPairProvider(Path... files) { + this(Arrays.asList(files)); + } + + public FileKeyPairProvider(Collection<? extends Path> files) { + this.files = files; + } + + public Collection<? extends Path> getPaths() { + return files; + } + + public void setFiles(Collection<File> files) { + setPaths(GenericUtils.map(files, File::toPath)); + } + + public void setPaths(Collection<? extends Path> paths) { + // use absolute path in order to have unique cache keys + Collection<Path> resolved = GenericUtils.map(paths, Path::toAbsolutePath); + resetCacheMap(resolved); + files = resolved; + } + + @Override + public Iterable<KeyPair> loadKeys() { + return loadKeys(getPaths()); + } + + @Override + protected KeyPair doLoadKey(Path resource) throws IOException, GeneralSecurityException { + return super.doLoadKey((resource == null) ? null : resource.toAbsolutePath()); + } + + @Override + protected InputStream openKeyPairResource(String resourceKey, Path resource) throws IOException { + return Files.newInputStream(resource, IoUtils.EMPTY_OPEN_OPTIONS); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java new file mode 100644 index 0000000..d0b35d9 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java @@ -0,0 +1,170 @@ +/* + * 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.common.keyprovider; + +import java.security.KeyPair; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.sshd.common.util.GenericUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface KeyIdentityProvider { + /** + * An "empty" implementation of {@link KeyIdentityProvider} that + * returns an empty group of key pairs + */ + KeyIdentityProvider EMPTY_KEYS_PROVIDER = new KeyIdentityProvider() { + @Override + public Iterable<KeyPair> loadKeys() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return "EMPTY"; + } + }; + + /** + * Invokes {@link KeyIdentityProvider#loadKeys()} and returns the result - ignores + * {@code null} providers (i.e., returns an empty iterable instance) + */ + Function<KeyIdentityProvider, Iterable<KeyPair>> LOADER = p -> + (p == null) ? Collections.<KeyPair>emptyList() : p.loadKeys(); + + /** + * Load available keys. + * + * @return an {@link Iterable} instance of available keys - ignored if {@code null} + */ + Iterable<KeyPair> loadKeys(); + + /** + * Creates a "unified" {@link Iterator} of {@link KeyPair}s out of 2 possible + * {@link KeyIdentityProvider} + * + * @param identities The registered keys identities + * @param keys Extra available key pairs + * @return The wrapping iterator + * @see #resolveKeyIdentityProvider(KeyIdentityProvider, KeyIdentityProvider) + */ + static Iterator<KeyPair> iteratorOf(KeyIdentityProvider identities, KeyIdentityProvider keys) { + return iteratorOf(resolveKeyIdentityProvider(identities, keys)); + } + + /** + * Resolves a non-{@code null} iterator of the available keys + * + * @param provider The {@link KeyIdentityProvider} - ignored if {@code null} + * @return A non-{@code null} iterator - which may be empty if no provider or no keys + */ + static Iterator<KeyPair> iteratorOf(KeyIdentityProvider provider) { + return GenericUtils.iteratorOf((provider == null) ? null : provider.loadKeys()); + } + + /** + * <P>Creates a "unified" {@link KeyIdentityProvider} out of 2 possible ones + * as follows:</P></BR> + * <UL> + * <LI>If both are {@code null} then return {@code null}.</LI> + * <LI>If either one is {@code null} then use the non-{@code null} one.</LI> + * <LI>If both are the same instance then use it.</U> + * <LI>Otherwise, returns a wrapper that groups both providers.</LI> + * </UL> + * @param identities The registered key pair identities + * @param keys The extra available key pairs + * @return The resolved provider + * @see #multiProvider(KeyIdentityProvider...) + */ + static KeyIdentityProvider resolveKeyIdentityProvider(KeyIdentityProvider identities, KeyIdentityProvider keys) { + if ((keys == null) || (identities == keys)) { + return identities; + } else if (identities == null) { + return keys; + } else { + return multiProvider(identities, keys); + } + } + + /** + * Wraps a group of {@link KeyIdentityProvider} into a single one + * + * @param providers The providers - ignored if {@code null}/empty (i.e., returns + * {@link #EMPTY_KEYS_PROVIDER}) + * @return The wrapping provider + * @see #multiProvider(Collection) + */ + static KeyIdentityProvider multiProvider(KeyIdentityProvider... providers) { + return multiProvider(GenericUtils.asList(providers)); + } + + /** + * Wraps a group of {@link KeyIdentityProvider} into a single one + * + * @param providers The providers - ignored if {@code null}/empty (i.e., returns + * {@link #EMPTY_KEYS_PROVIDER}) + * @return The wrapping provider + */ + static KeyIdentityProvider multiProvider(Collection<? extends KeyIdentityProvider> providers) { + return GenericUtils.isEmpty(providers) ? EMPTY_KEYS_PROVIDER : wrapKeyPairs(iterableOf(providers)); + } + + /** + * Wraps a group of {@link KeyIdentityProvider} into an {@link Iterable} of {@link KeyPair}s + * + * @param providers The group of providers - ignored if {@code null}/empty (i.e., returns an + * empty iterable instance) + * @return The wrapping iterable + */ + static Iterable<KeyPair> iterableOf(Collection<? extends KeyIdentityProvider> providers) { + Iterable<Supplier<Iterable<KeyPair>>> keysSuppliers = + GenericUtils.<KeyIdentityProvider, Supplier<Iterable<KeyPair>>>wrapIterable(providers, p -> p::loadKeys); + return GenericUtils.multiIterableSuppliers(keysSuppliers); + } + + /** + * Wraps a group of {@link KeyPair}s into a {@link KeyIdentityProvider} + * + * @param pairs The key pairs - ignored if {@code null}/empty (i.e., returns + * {@link #EMPTY_KEYS_PROVIDER}). + * @return The provider wrapper + */ + static KeyIdentityProvider wrapKeyPairs(KeyPair... pairs) { + return wrapKeyPairs(GenericUtils.asList(pairs)); + } + + /** + * Wraps a group of {@link KeyPair}s into a {@link KeyIdentityProvider} + * + * @param pairs The key pairs {@link Iterable} - ignored if {@code null} (i.e., returns + * {@link #EMPTY_KEYS_PROVIDER}). + * @return The provider wrapper + */ + static KeyIdentityProvider wrapKeyPairs(Iterable<KeyPair> pairs) { + return (pairs == null) ? EMPTY_KEYS_PROVIDER : () -> pairs; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java new file mode 100644 index 0000000..a816304 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java @@ -0,0 +1,181 @@ +/* + * 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.common.keyprovider; + +import java.security.KeyPair; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * Provider for key pairs. This provider is used on the server side to provide + * the host key, or on the client side to provide the user key. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KeyPairProvider extends KeyIdentityProvider { + + /** + * SSH identifier for RSA keys + */ + String SSH_RSA = "ssh-rsa"; + + /** + * SSH identifier for DSA keys + */ + String SSH_DSS = "ssh-dss"; + + /** + * SSH identifier for ED25519 elliptic curve keys + */ + String SSH_ED25519 = "ssh-ed25519"; + + /** + * SSH identifier for EC keys in NIST curve P-256 + */ + String ECDSA_SHA2_NISTP256 = ECCurves.nistp256.getKeyType(); + + /** + * SSH identifier for EC keys in NIST curve P-384 + */ + String ECDSA_SHA2_NISTP384 = ECCurves.nistp384.getKeyType(); + + /** + * SSH identifier for EC keys in NIST curve P-521 + */ + String ECDSA_SHA2_NISTP521 = ECCurves.nistp521.getKeyType(); + + /** + * A {@link KeyPairProvider} that has no keys + */ + KeyPairProvider EMPTY_KEYPAIR_PROVIDER = + new KeyPairProvider() { + @Override + public KeyPair loadKey(String type) { + return null; + } + + @Override + public Iterable<String> getKeyTypes() { + return Collections.emptyList(); + } + + @Override + public Iterable<KeyPair> loadKeys() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return "EMPTY_KEYPAIR_PROVIDER"; + } + }; + + /** + * Load a key of the specified type which can be "ssh-rsa", "ssh-dss", + * or "ecdsa-sha2-nistp{256,384,521}". If there is no key of this type, return + * {@code null} + * + * @param type the type of key to load + * @return a valid key pair or {@code null} if this type of key is not available + */ + default KeyPair loadKey(String type) { + ValidateUtils.checkNotNullAndNotEmpty(type, "No key type to load"); + return GenericUtils.stream(loadKeys()) + .filter(key -> type.equals(KeyUtils.getKeyType(key))) + .findFirst() + .orElse(null); + } + + /** + * @return The available {@link Iterable} key types in preferred order - never {@code null} + */ + default Iterable<String> getKeyTypes() { + return GenericUtils.stream(loadKeys()) + .map(KeyUtils::getKeyType) + .filter(GenericUtils::isNotEmpty) + .collect(Collectors.toSet()); + } + + /** + * Wrap the provided {@link KeyPair}s into a {@link KeyPairProvider} + * + * @param pairs The available pairs - ignored if {@code null}/empty (i.e., + * returns {@link #EMPTY_KEYPAIR_PROVIDER}) + * @return The provider wrapper + * @see #wrap(Iterable) + */ + static KeyPairProvider wrap(KeyPair... pairs) { + return GenericUtils.isEmpty(pairs) ? EMPTY_KEYPAIR_PROVIDER : wrap(Arrays.asList(pairs)); + } + + /** + * Wrap the provided {@link KeyPair}s into a {@link KeyPairProvider} + * + * @param pairs The available pairs {@link Iterable} - ignored if {@code null} (i.e., + * returns {@link #EMPTY_KEYPAIR_PROVIDER}) + * @return The provider wrapper + */ + static KeyPairProvider wrap(Iterable<KeyPair> pairs) { + return (pairs == null) ? EMPTY_KEYPAIR_PROVIDER : new KeyPairProvider() { + @Override + public Iterable<KeyPair> loadKeys() { + return pairs; + } + + @Override + public KeyPair loadKey(String type) { + for (KeyPair kp : pairs) { + String t = KeyUtils.getKeyType(kp); + if (Objects.equals(type, t)) { + return kp; + } + } + + return null; + } + + @Override + public Iterable<String> getKeyTypes() { + // use a LinkedHashSet so as to preserve the order but avoid duplicates + Collection<String> types = new LinkedHashSet<>(); + for (KeyPair kp : pairs) { + String t = KeyUtils.getKeyType(kp); + if (GenericUtils.isEmpty(t)) { + continue; // avoid unknown key types + } + + if (!types.add(t)) { + continue; // debug breakpoint + } + } + + return types; + } + }; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProviderHolder.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProviderHolder.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProviderHolder.java new file mode 100644 index 0000000..553d553 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProviderHolder.java @@ -0,0 +1,35 @@ +/* + * 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.common.keyprovider; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KeyPairProviderHolder { + /** + * Retrieve the <code>KeyPairProvider</code> that will be used to find + * the host key to use on the server side or the user key on the client side. + * + * @return the <code>KeyPairProvider</code>, never {@code null} + */ + KeyPairProvider getKeyPairProvider(); + + void setKeyPairProvider(KeyPairProvider keyPairProvider); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MappedKeyPairProvider.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MappedKeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MappedKeyPairProvider.java new file mode 100644 index 0000000..bf3ec8b --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MappedKeyPairProvider.java @@ -0,0 +1,97 @@ +/* + * 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.common.keyprovider; + +import java.security.KeyPair; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; + +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * Holds a {@link Map} of {@link String}->{@link KeyPair} where the map key + * is the type and value is the associated {@link KeyPair} + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class MappedKeyPairProvider implements KeyPairProvider { + /** + * Transforms a {@link Map} of {@link String}->{@link KeyPair} to a + * {@link KeyPairProvider} where map key is the type and value is the + * associated {@link KeyPair} + */ + public static final Function<Map<String, KeyPair>, KeyPairProvider> MAP_TO_KEY_PAIR_PROVIDER = + MappedKeyPairProvider::new; + + private final Map<String, KeyPair> pairsMap; + + public MappedKeyPairProvider(KeyPair... pairs) { + this(GenericUtils.isEmpty(pairs) ? Collections.emptyList() : Arrays.asList(pairs)); + } + + public MappedKeyPairProvider(Collection<? extends KeyPair> pairs) { + this(mapUniquePairs(pairs)); + } + + public MappedKeyPairProvider(Map<String, KeyPair> pairsMap) { + this.pairsMap = ValidateUtils.checkNotNullAndNotEmpty(pairsMap, "No pairs map provided"); + } + + @Override + public Iterable<KeyPair> loadKeys() { + return pairsMap.values(); + } + + @Override + public KeyPair loadKey(String type) { + return pairsMap.get(type); + } + + @Override + public Iterable<String> getKeyTypes() { + return pairsMap.keySet(); + } + + @Override + public String toString() { + return String.valueOf(getKeyTypes()); + } + + public static Map<String, KeyPair> mapUniquePairs(Collection<? extends KeyPair> pairs) { + if (GenericUtils.isEmpty(pairs)) { + return Collections.emptyMap(); + } + + Map<String, KeyPair> pairsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (KeyPair kp : pairs) { + String keyType = ValidateUtils.checkNotNullAndNotEmpty(KeyUtils.getKeyType(kp), "Cannot determine key type"); + KeyPair prev = pairsMap.put(keyType, kp); + ValidateUtils.checkTrue(prev == null, "Multiple keys of type=%s", keyType); + } + + return pairsMap; + } +}
