All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sshd.common.kex.extension.KexExtensions Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 34.0.0.Final
Show newest version
/*
 * 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.kex.extension;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.kex.extension.parser.DelayCompression;
import org.apache.sshd.common.kex.extension.parser.Elevation;
import org.apache.sshd.common.kex.extension.parser.NoFlowControl;
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.Readable;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;

/**
 * Provides some helpers for RFC 8308
 *
 * @author Apache MINA SSHD Project
 */
public final class KexExtensions {
    public static final byte SSH_MSG_EXT_INFO = 7;
    public static final byte SSH_MSG_NEWCOMPRESS = 8;

    public static final String CLIENT_KEX_EXTENSION = "ext-info-c";
    public static final String SERVER_KEX_EXTENSION = "ext-info-s";

    public static final Predicate IS_KEX_EXTENSION_SIGNAL = //
            n -> CLIENT_KEX_EXTENSION.equalsIgnoreCase(n) || SERVER_KEX_EXTENSION.equalsIgnoreCase(n);

    /**
     * Reminder:
     *
     * These pseudo-algorithms are only valid in the initial SSH2_MSG_KEXINIT and MUST be ignored if they are present in
     * subsequent SSH2_MSG_KEXINIT packets.
     *
     * Note: these values are appended to the initial proposals and removed if received before proceeding
     * with the standard KEX proposals negotiation.
     *
     * @see OpenSSH PROTOCOL - 1.9 transport:
     *      strict key exchange extension
     */
    public static final String STRICT_KEX_CLIENT_EXTENSION = "[email protected]";
    public static final String STRICT_KEX_SERVER_EXTENSION = "[email protected]";

    /**
     * A case insensitive map of all the default known {@link KexExtensionParser} where key=the extension name
     */
    private static final NavigableMap> EXTENSION_PARSERS = Stream.of(
            ServerSignatureAlgorithms.INSTANCE,
            NoFlowControl.INSTANCE,
            Elevation.INSTANCE,
            DelayCompression.INSTANCE)
            .collect(Collectors.toMap(
                    NamedResource::getName, Function.identity(),
                    MapEntryUtils.throwingMerger(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));

    private KexExtensions() {
        throw new UnsupportedOperationException("No instance allowed");
    }

    /**
     * @return A case insensitive copy of the currently registered {@link KexExtensionParser}s names
     */
    public static NavigableSet getRegisteredExtensionParserNames() {
        synchronized (EXTENSION_PARSERS) {
            return EXTENSION_PARSERS.isEmpty()
                    ? Collections.emptyNavigableSet()
                    : GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, EXTENSION_PARSERS.keySet());
        }
    }

    /**
     * @param  name The (never {@code null}/empty) extension name
     * @return      The registered {@code KexExtensionParser} for the (case insensitive) extension name -
     *              {@code null} if no match found
     */
    public static KexExtensionParser getRegisteredExtensionParser(String name) {
        ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name provided");
        synchronized (EXTENSION_PARSERS) {
            return EXTENSION_PARSERS.get(name);
        }
    }

    /**
     * Registers a {@link KexExtensionParser} for a named extension
     *
     * @param  parser The (never {@code null}) parser to register
     * @return        The replaced parser for the named extension (case insensitive) - {@code null} if no
     *                previous parser registered for this extension
     */
    public static KexExtensionParser registerExtensionParser(KexExtensionParser parser) {
        Objects.requireNonNull(parser, "No parser provided");
        String name = ValidateUtils.checkNotNullAndNotEmpty(parser.getName(), "No extension name provided");
        synchronized (EXTENSION_PARSERS) {
            return EXTENSION_PARSERS.put(name, parser);
        }
    }

    /**
     * Registers {@link KexExtensionParser} for a named extension
     *
     * @param  name The (never {@code null}/empty) extension name
     * @return      The removed {@code KexExtensionParser} for the (case insensitive) extension name -
     *              {@code null} if no match found
     */
    public static KexExtensionParser unregisterExtensionParser(String name) {
        ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name provided");
        synchronized (EXTENSION_PARSERS) {
            return EXTENSION_PARSERS.remove(name);
        }
    }

    /**
     * Attempts to parse an {@code SSH_MSG_EXT_INFO} message
     *
     * @param  buffer      The {@link Buffer} containing the message
     * @return             A {@link List} of key/value "pairs" where key=the extension name, value=the parsed
     *                     value using the matching registered {@link KexExtensionParser}. If no such parser found then
     *                     the raw value bytes are set as the extension value.
     * @throws IOException If failed to parse one of the extensions
     * @see                RFC-8308 - section 2.3
     */
    public static List> parseExtensions(Buffer buffer) throws IOException {
        int count = buffer.getInt();
        // Protect agains malicious packets
        ValidateUtils.checkTrue(count >= 0, "Invalid extensions count: %d", count);
        if (count == 0) {
            return Collections.emptyList();
        }

        List> entries = new ArrayList<>(count);
        for (int index = 0; index < count; index++) {
            String name = buffer.getString();
            byte[] data = buffer.getBytes();
            KexExtensionParser parser = getRegisteredExtensionParser(name);
            Object value = (parser == null) ? data : parser.parseExtension(data);
            entries.add(new SimpleImmutableEntry<>(name, value));
        }

        return entries;
    }

    /**
     * Creates an {@code SSH_MSG_EXT_INFO} message using the provided extensions.
     *
     * @param  exts        A {@link Collection} of key/value "pairs" where key=the extension name, value=the
     *                     extension value. Note: if a registered {@link KexExtensionParser} exists for the name,
     *                     then it is assumed that the value is of the correct type. If no registered parser found the
     *                     value is assumed to be either the encoded value as an array of bytes or as another
     *                     {@link Readable} (e.g., another {@link Buffer}) or a {@link ByteBuffer}.
     * @param  buffer      The target {@link Buffer} - assumed to already contain the {@code SSH_MSG_EXT_INFO} opcode
     * @throws IOException If failed to encode
     */
    public static void putExtensions(Collection> exts, Buffer buffer) throws IOException {
        int count = GenericUtils.size(exts);
        buffer.putUInt(count);
        if (count <= 0) {
            return;
        }

        for (Map.Entry ee : exts) {
            String name = ee.getKey();
            Object value = ee.getValue();
            @SuppressWarnings("unchecked")
            KexExtensionParser parser = (KexExtensionParser) getRegisteredExtensionParser(name);
            if (parser != null) {
                parser.putExtension(value, buffer);
            } else {
                buffer.putOptionalBufferedData(value);
            }
        }
    }
}