Bind SshClient/SshServer to NettyIoServiceFactoryFactory
[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 && clientIdentity.getNone() == null) {
128                 setClientIdentity(ret, clientIdentity);
129             }
130             ret.setIoServiceFactoryFactory(ioServiceFactory);
131             ret.setScheduledExecutorService(group);
132
133             try {
134                 ret.checkConfig();
135             } catch (IllegalArgumentException e) {
136                 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
137             }
138             return ret;
139         }
140
141         /**
142          * Guaranteed to throw an exception.
143          *
144          * @throws UnsupportedOperationException always
145          */
146         @Override
147         @Deprecated(forRemoval = true)
148         @DoNotCall("Always throws UnsupportedOperationException")
149         public TransportSshClient build() {
150             throw new UnsupportedOperationException();
151         }
152
153         /**
154          * Guaranteed to throw an exception.
155          *
156          * @throws UnsupportedOperationException always
157          */
158         @Override
159         @Deprecated(forRemoval = true)
160         @DoNotCall("Always throws UnsupportedOperationException")
161         public TransportSshClient build(final boolean isFillWithDefaultValues) {
162             throw new UnsupportedOperationException();
163         }
164
165         @Override
166         protected ClientBuilder fillWithDefaultValues() {
167             if (factory == null) {
168                 factory = TransportSshClient::new;
169             }
170             return super.fillWithDefaultValues();
171         }
172
173         private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
174                 throws UnsupportedConfigurationException {
175             final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
176             final var password = clientIdentity.getPassword();
177             if (password != null) {
178                 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
179                     client.setPasswordIdentityProvider(
180                             PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
181                     authFactoriesListBuilder.add(new UserAuthPasswordFactory());
182                 }
183                 // TODO support encrypted password -- requires augmentation of default schema
184             }
185             final var hostBased = clientIdentity.getHostbased();
186             if (hostBased != null) {
187                 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
188                 var factory = new UserAuthHostBasedFactory();
189                 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
190                 factory.setClientUsername(clientIdentity.getUsername());
191                 factory.setClientHostname(null); // not provided via config
192                 factory.setSignatureFactories(client.getSignatureFactories());
193                 authFactoriesListBuilder.add(factory);
194             }
195             final var publicKey = clientIdentity.getPublicKey();
196             if (publicKey != null) {
197                 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
198                 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
199                 final var factory = new UserAuthPublicKeyFactory();
200                 factory.setSignatureFactories(client.getSignatureFactories());
201                 authFactoriesListBuilder.add(factory);
202             }
203             // FIXME implement authentication using X509 certificate
204             final var userAuthFactories = authFactoriesListBuilder.build();
205             if (userAuthFactories.isEmpty()) {
206                 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
207             }
208             client.setUserAuthFactories(userAuthFactories);
209         }
210     }
211 }