Require a subsystem for client connections
[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 io.netty.channel.EventLoopGroup;
15 import java.security.cert.Certificate;
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.rev230417.password.grouping.password.type.CleartextPassword;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentity;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.Keepalives;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ServerAuthentication;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.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 EventLoopGroup group;
74
75         private Keepalives keepAlives;
76         private ClientIdentity clientIdentity;
77
78         Builder(final NettyIoServiceFactoryFactory ioServiceFactory, final EventLoopGroup group) {
79             this.ioServiceFactory = requireNonNull(ioServiceFactory);
80             this.group = requireNonNull(group);
81         }
82
83         Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
84             ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
85             return this;
86         }
87
88         Builder keepAlives(final Keepalives newkeepAlives) {
89             keepAlives = newkeepAlives;
90             return this;
91         }
92
93         Builder clientIdentity(final ClientIdentity newClientIdentity) {
94             clientIdentity = newClientIdentity;
95             return this;
96         }
97
98         Builder serverAuthentication(final ServerAuthentication serverAuthentication)
99                 throws UnsupportedConfigurationException {
100             final ServerKeyVerifier newVerifier;
101             if (serverAuthentication != null) {
102                 final var certificatesList = ImmutableList.<Certificate>builder()
103                     .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
104                     .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
105                     .build();
106                 final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
107                 if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
108                     throw new UnsupportedConfigurationException(
109                         "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
110                 }
111                 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
112             } else {
113                 newVerifier = null;
114             }
115
116             serverKeyVerifier(newVerifier);
117             return this;
118         }
119
120         TransportSshClient buildChecked() throws UnsupportedConfigurationException {
121             final var ret = (TransportSshClient) super.build(true);
122             if (keepAlives != null) {
123                 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
124             } else {
125                 ConfigUtils.setKeepAlives(ret, null, null);
126             }
127             if (clientIdentity == null) {
128                 throw new UnsupportedConfigurationException("Client parameters are required");
129             }
130             final var username = clientIdentity.getUsername();
131             if (username == null) {
132                 throw new UnsupportedConfigurationException("Client parameters are missing username");
133             }
134
135             if (clientIdentity != null && clientIdentity.getNone() == null) {
136                 setClientIdentity(ret, clientIdentity);
137             }
138             ret.setIoServiceFactoryFactory(ioServiceFactory);
139             ret.setScheduledExecutorService(group);
140
141             try {
142                 ret.checkConfig();
143             } catch (IllegalArgumentException e) {
144                 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
145             }
146
147             ret.setSessionFactory(new TransportClientSessionFactory(ret, username));
148             return ret;
149         }
150
151         /**
152          * Guaranteed to throw an exception.
153          *
154          * @throws UnsupportedOperationException always
155          */
156         @Override
157         @Deprecated(forRemoval = true)
158         @DoNotCall("Always throws UnsupportedOperationException")
159         public TransportSshClient build() {
160             throw new UnsupportedOperationException();
161         }
162
163         /**
164          * Guaranteed to throw an exception.
165          *
166          * @throws UnsupportedOperationException always
167          */
168         @Override
169         @Deprecated(forRemoval = true)
170         @DoNotCall("Always throws UnsupportedOperationException")
171         public TransportSshClient build(final boolean isFillWithDefaultValues) {
172             throw new UnsupportedOperationException();
173         }
174
175         @Override
176         protected ClientBuilder fillWithDefaultValues() {
177             if (factory == null) {
178                 factory = TransportSshClient::new;
179             }
180             return super.fillWithDefaultValues();
181         }
182
183         private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
184                 throws UnsupportedConfigurationException {
185             final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
186             final var password = clientIdentity.getPassword();
187             if (password != null) {
188                 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
189                     client.setPasswordIdentityProvider(
190                             PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
191                     authFactoriesListBuilder.add(new UserAuthPasswordFactory());
192                 }
193                 // TODO support encrypted password -- requires augmentation of default schema
194             }
195             final var hostBased = clientIdentity.getHostbased();
196             if (hostBased != null) {
197                 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
198                 var factory = new UserAuthHostBasedFactory();
199                 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
200                 factory.setClientUsername(clientIdentity.getUsername());
201                 factory.setClientHostname(null); // not provided via config
202                 factory.setSignatureFactories(client.getSignatureFactories());
203                 authFactoriesListBuilder.add(factory);
204             }
205             final var publicKey = clientIdentity.getPublicKey();
206             if (publicKey != null) {
207                 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
208                 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
209                 final var factory = new UserAuthPublicKeyFactory();
210                 factory.setSignatureFactories(client.getSignatureFactories());
211                 authFactoriesListBuilder.add(factory);
212             }
213             // FIXME implement authentication using X509 certificate
214             final var userAuthFactories = authFactoriesListBuilder.build();
215             if (userAuthFactories.isEmpty()) {
216                 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
217             }
218             client.setUserAuthFactories(userAuthFactories);
219         }
220     }
221 }