Refresh IETF client/server models
[netconf.git] / transport / transport-ssh / src / main / java / org / opendaylight / netconf / transport / ssh / TransportSshClient.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.transport.ssh;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.ImmutableList;
13 import com.google.errorprone.annotations.DoNotCall;
14 import java.security.cert.Certificate;
15 import java.util.concurrent.ScheduledExecutorService;
16 import org.opendaylight.netconf.shaded.sshd.client.ClientBuilder;
17 import org.opendaylight.netconf.shaded.sshd.client.SshClient;
18 import org.opendaylight.netconf.shaded.sshd.client.auth.UserAuthFactory;
19 import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.HostKeyIdentityProvider;
20 import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.UserAuthHostBasedFactory;
21 import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
22 import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
23 import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
24 import org.opendaylight.netconf.shaded.sshd.client.keyverifier.ServerKeyVerifier;
25 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
26 import org.opendaylight.netconf.shaded.sshd.netty.NettyIoServiceFactoryFactory;
27 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.password.grouping.password.type.CleartextPassword;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentity;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.Keepalives;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ServerAuthentication;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev231228.TransportParamsGrouping;
33
34 /**
35  * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
36  * it to be started.
37  */
38 final class TransportSshClient extends SshClient {
39     private TransportSshClient() {
40         // Hidden on purpose
41     }
42
43     /**
44      * Guaranteed to throw an exception.
45      *
46      * @throws UnsupportedOperationException always
47      */
48     @Override
49     @Deprecated(forRemoval = true)
50     @DoNotCall("Always throws UnsupportedOperationException")
51     public void start() {
52         throw new UnsupportedOperationException();
53     }
54
55     /**
56      * Guaranteed to throw an exception.
57      *
58      * @throws UnsupportedOperationException always
59      */
60     @Override
61     @Deprecated(forRemoval = true)
62     @DoNotCall("Always throws UnsupportedOperationException")
63     public void stop() {
64         throw new UnsupportedOperationException();
65     }
66
67     /**
68      * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
69      * {@code ietf-netconf-client.yang} configuration.
70      */
71     static final class Builder extends ClientBuilder {
72         private final NettyIoServiceFactoryFactory ioServiceFactory;
73         private final ScheduledExecutorService executorService;
74
75         private ClientFactoryManagerConfigurator configurator;
76         private Keepalives keepAlives;
77         private ClientIdentity clientIdentity;
78
79         Builder(final NettyIoServiceFactoryFactory ioServiceFactory, final ScheduledExecutorService executorService) {
80             this.ioServiceFactory = requireNonNull(ioServiceFactory);
81             this.executorService = requireNonNull(executorService);
82         }
83
84         Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
85             ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
86             return this;
87         }
88
89         Builder keepAlives(final Keepalives newKeepAlives) {
90             keepAlives = newKeepAlives;
91             return this;
92         }
93
94         Builder clientIdentity(final ClientIdentity newClientIdentity) {
95             clientIdentity = newClientIdentity;
96             return this;
97         }
98
99         Builder serverAuthentication(final ServerAuthentication serverAuthentication)
100                 throws UnsupportedConfigurationException {
101             final ServerKeyVerifier newVerifier;
102             if (serverAuthentication != null) {
103                 final var certificatesList = ImmutableList.<Certificate>builder()
104                     .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
105                     .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
106                     .build();
107                 final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
108                 if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
109                     throw new UnsupportedConfigurationException(
110                         "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
111                 }
112                 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
113             } else {
114                 newVerifier = null;
115             }
116
117             serverKeyVerifier(newVerifier);
118             return this;
119         }
120
121         Builder configurator(final ClientFactoryManagerConfigurator newConfigurator) {
122             configurator = newConfigurator;
123             return this;
124         }
125
126         TransportSshClient buildChecked() throws UnsupportedConfigurationException {
127             final var ret = (TransportSshClient) super.build(true);
128             if (keepAlives != null) {
129                 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
130             } else {
131                 ConfigUtils.setKeepAlives(ret, null, null);
132             }
133             if (clientIdentity == null) {
134                 throw new UnsupportedConfigurationException("Client parameters are required");
135             }
136             final var username = clientIdentity.getUsername();
137             if (username == null) {
138                 throw new UnsupportedConfigurationException("Client parameters are missing username");
139             }
140
141             if (clientIdentity != null && clientIdentity.getNone() == null) {
142                 setClientIdentity(ret, clientIdentity, configurator == null);
143             }
144             if (configurator != null) {
145                 configurator.configureClientFactoryManager(ret);
146             }
147             ret.setIoServiceFactoryFactory(ioServiceFactory);
148             ret.setScheduledExecutorService(executorService);
149
150             try {
151                 ret.checkConfig();
152             } catch (IllegalArgumentException e) {
153                 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
154             }
155
156             ret.setSessionFactory(new TransportClientSessionFactory(ret, username));
157             return ret;
158         }
159
160         /**
161          * Guaranteed to throw an exception.
162          *
163          * @throws UnsupportedOperationException always
164          */
165         @Override
166         @Deprecated(forRemoval = true)
167         @DoNotCall("Always throws UnsupportedOperationException")
168         public TransportSshClient build() {
169             throw new UnsupportedOperationException();
170         }
171
172         /**
173          * Guaranteed to throw an exception.
174          *
175          * @throws UnsupportedOperationException always
176          */
177         @Override
178         @Deprecated(forRemoval = true)
179         @DoNotCall("Always throws UnsupportedOperationException")
180         public TransportSshClient build(final boolean isFillWithDefaultValues) {
181             throw new UnsupportedOperationException();
182         }
183
184         @Override
185         protected ClientBuilder fillWithDefaultValues() {
186             if (factory == null) {
187                 factory = TransportSshClient::new;
188             }
189             return super.fillWithDefaultValues();
190         }
191
192         private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity,
193                 final boolean throwExceptionIfNoAuthMethodDefined) throws UnsupportedConfigurationException {
194             final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
195             final var password = clientIdentity.getPassword();
196             if (password != null) {
197                 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
198                     client.setPasswordIdentityProvider(
199                             PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
200                     authFactoriesListBuilder.add(new UserAuthPasswordFactory());
201                 }
202                 // TODO support encrypted password -- requires augmentation of default schema
203             }
204             final var hostBased = clientIdentity.getHostbased();
205             if (hostBased != null) {
206                 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
207                 var factory = new UserAuthHostBasedFactory();
208                 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
209                 factory.setClientUsername(clientIdentity.getUsername());
210                 // not provided via config
211                 factory.setClientHostname(null);
212                 factory.setSignatureFactories(client.getSignatureFactories());
213                 authFactoriesListBuilder.add(factory);
214             }
215             final var publicKey = clientIdentity.getPublicKey();
216             if (publicKey != null) {
217                 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
218                 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
219                 final var factory = new UserAuthPublicKeyFactory();
220                 factory.setSignatureFactories(client.getSignatureFactories());
221                 authFactoriesListBuilder.add(factory);
222             }
223             // FIXME implement authentication using X509 certificate
224
225             final var userAuthFactories = authFactoriesListBuilder.build();
226             if (!userAuthFactories.isEmpty()) {
227                 client.setUserAuthFactories(userAuthFactories);
228             } else if (throwExceptionIfNoAuthMethodDefined) {
229                 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
230             }
231         }
232     }
233 }