Bind SshClient/SshServer to NettyIoServiceFactoryFactory
[netconf.git] / transport / transport-ssh / src / main / java / org / opendaylight / netconf / transport / ssh / TransportSshServer.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.common.collect.ImmutableMap;
14 import com.google.errorprone.annotations.DoNotCall;
15 import io.netty.channel.EventLoopGroup;
16 import java.security.PublicKey;
17 import java.util.List;
18 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider;
19 import org.opendaylight.netconf.shaded.sshd.netty.NettyIoServiceFactoryFactory;
20 import org.opendaylight.netconf.shaded.sshd.server.ServerBuilder;
21 import org.opendaylight.netconf.shaded.sshd.server.SshServer;
22 import org.opendaylight.netconf.shaded.sshd.server.auth.UserAuthFactory;
23 import org.opendaylight.netconf.shaded.sshd.server.auth.hostbased.UserAuthHostBasedFactory;
24 import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
25 import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.UserAuthPublicKeyFactory;
26 import org.opendaylight.netconf.shaded.sshd.server.subsystem.SubsystemFactory;
27 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.SshServerGrouping;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ClientAuthentication;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.Keepalives;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentity;
32
33 /**
34  * Our internal-use {@link SshServer}. We reuse all the properties and logic of an {@link SshServer}, but we never allow
35  * it to be started.
36  */
37 final class TransportSshServer extends SshServer {
38     private TransportSshServer() {
39         // Hidden on purpose
40     }
41
42     /**
43      * Guaranteed to throw an exception.
44      *
45      * @throws UnsupportedOperationException always
46      */
47     @Override
48     @Deprecated(forRemoval = true)
49     @DoNotCall("Always throws UnsupportedOperationException")
50     public void start() {
51         throw new UnsupportedOperationException();
52     }
53
54     /**
55      * Guaranteed to throw an exception.
56      *
57      * @throws UnsupportedOperationException always
58      */
59     @Override
60     @Deprecated(forRemoval = true)
61     @DoNotCall("Always throws UnsupportedOperationException")
62     public void stop() {
63         throw new UnsupportedOperationException();
64     }
65
66     /**
67      * A {@link ServerBuilder} producing {@link TransportSshServer}s. Also hosts adaptation from
68      * {@code ietf-netconf-server.yang} configuration.
69      */
70     static final class Builder extends ServerBuilder {
71         private final NettyIoServiceFactoryFactory ioServiceFactory;
72         private final EventLoopGroup group;
73         private final SubsystemFactory subsystemFactory;
74
75         private ServerFactoryManagerConfigurator configurator;
76         private ClientAuthentication clientAuthentication;
77         private ServerIdentity serverIdentity;
78         private Keepalives keepAlives;
79
80         Builder(final NettyIoServiceFactoryFactory ioServiceFactory, final EventLoopGroup group,
81                 final SubsystemFactory subsystemFactory) {
82             this.ioServiceFactory = requireNonNull(ioServiceFactory);
83             this.group = requireNonNull(group);
84             this.subsystemFactory = requireNonNull(subsystemFactory);
85         }
86
87         Builder serverParams(final SshServerGrouping serverParams) throws UnsupportedConfigurationException {
88             if (serverParams != null) {
89                 ConfigUtils.setTransportParams(this, serverParams.getTransportParams(),
90                     TransportUtils::getServerKexFactories);
91                 keepAlives = serverParams.getKeepalives();
92                 serverIdentity = serverParams.getServerIdentity();
93                 if (serverIdentity == null) {
94                     throw new UnsupportedConfigurationException("Server identity configuration is required");
95                 }
96                 clientAuthentication = serverParams.getClientAuthentication();
97             }
98             return this;
99         }
100
101         Builder configurator(final ServerFactoryManagerConfigurator newConfigurator) {
102             configurator = newConfigurator;
103             return this;
104         }
105
106         /**
107          * Guaranteed to throw an exception.
108          *
109          * @throws UnsupportedOperationException always
110          */
111         @Override
112         @Deprecated(forRemoval = true)
113         @DoNotCall("Always throws UnsupportedOperationException")
114         public TransportSshServer build() {
115             throw new UnsupportedOperationException();
116         }
117
118         /**
119          * Guaranteed to throw an exception.
120          *
121          * @throws UnsupportedOperationException always
122          */
123         @Override
124         @Deprecated(forRemoval = true)
125         @DoNotCall("Always throws UnsupportedOperationException")
126         public TransportSshServer build(final boolean isFillWithDefaultValues) {
127             throw new UnsupportedOperationException();
128         }
129
130         TransportSshServer buildChecked() throws UnsupportedConfigurationException {
131             final var ret = (TransportSshServer) super.build(true);
132             if (keepAlives != null) {
133                 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
134             } else {
135                 ConfigUtils.setKeepAlives(ret, null, null);
136             }
137             if (serverIdentity != null) {
138                 setServerIdentity(ret, serverIdentity);
139             }
140             if (clientAuthentication != null) {
141                 setClientAuthentication(ret, clientAuthentication);
142             }
143             if (configurator != null) {
144                 configurator.configureServerFactoryManager(ret);
145             }
146
147             ret.setSubsystemFactories(List.of(subsystemFactory));
148             ret.setIoServiceFactoryFactory(ioServiceFactory);
149             ret.setScheduledExecutorService(group);
150
151             try {
152                 ret.checkConfig();
153             } catch (IllegalArgumentException e) {
154                 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
155             }
156             return ret;
157         }
158
159         @Override
160         protected ServerBuilder fillWithDefaultValues() {
161             if (factory == null) {
162                 factory = TransportSshServer::new;
163             }
164             return super.fillWithDefaultValues();
165         }
166
167         private static void setServerIdentity(final TransportSshServer server, final ServerIdentity serverIdentity)
168                 throws UnsupportedConfigurationException {
169             final var hostKey = serverIdentity.getHostKey();
170             if (hostKey == null || hostKey.isEmpty()) {
171                 throw new UnsupportedConfigurationException("Host keys is missing in server identity configuration");
172             }
173             final var serverHostKeyPairs = ConfigUtils.extractServerHostKeys(hostKey);
174             if (!serverHostKeyPairs.isEmpty()) {
175                 server.setKeyPairProvider(KeyPairProvider.wrap(serverHostKeyPairs));
176             }
177         }
178
179         private static void setClientAuthentication(final TransportSshServer server,
180                 final ClientAuthentication clientAuthentication) throws UnsupportedConfigurationException {
181             final var users = clientAuthentication.getUsers();
182             if (users == null) {
183                 return;
184             }
185             final var userMap = users.getUser();
186             if (userMap != null) {
187                 final var passwordMapBuilder = ImmutableMap.<String, String>builder();
188                 final var hostBasedMapBuilder = ImmutableMap.<String, List<PublicKey>>builder();
189                 final var publicKeyMapBuilder = ImmutableMap.<String, List<PublicKey>>builder();
190                 for (var entry : userMap.entrySet()) {
191                     final var username = entry.getKey().getName();
192                     final var value = entry.getValue();
193                     final var password = value.getPassword();
194                     if (password != null) {
195                         passwordMapBuilder.put(username, password.getValue());
196                     }
197                     final var hostBased = value.getHostbased();
198                     if (hostBased != null) {
199                         hostBasedMapBuilder.put(username,
200                             ConfigUtils.extractPublicKeys(hostBased.getInlineOrTruststore()));
201                     }
202                     final var publicKey = value.getPublicKeys();
203                     if (publicKey != null) {
204                         publicKeyMapBuilder.put(username,
205                             ConfigUtils.extractPublicKeys(publicKey.getInlineOrTruststore()));
206                     }
207                 }
208                 final var authFactoriesBuilder = ImmutableList.<UserAuthFactory>builder();
209                 final var passwordMap = passwordMapBuilder.build();
210                 if (!passwordMap.isEmpty()) {
211                     authFactoriesBuilder.add(new UserAuthPasswordFactory());
212                     server.setPasswordAuthenticator(new CryptHashPasswordAuthenticator(passwordMap));
213                 }
214                 final var hostBasedMap = hostBasedMapBuilder.build();
215                 if (!hostBasedMap.isEmpty()) {
216                     final var factory = new UserAuthHostBasedFactory();
217                     factory.setSignatureFactories(server.getSignatureFactories());
218                     authFactoriesBuilder.add(factory);
219                     server.setHostBasedAuthenticator(new UserPublicKeyAuthenticator(hostBasedMap));
220                 }
221                 final var publicKeyMap = publicKeyMapBuilder.build();
222                 if (!publicKeyMap.isEmpty()) {
223                     final var factory = new UserAuthPublicKeyFactory();
224                     factory.setSignatureFactories(server.getSignatureFactories());
225                     authFactoriesBuilder.add(factory);
226                     server.setPublickeyAuthenticator(new UserPublicKeyAuthenticator(publicKeyMap));
227                 }
228                 server.setUserAuthFactories(authFactoriesBuilder.build());
229             }
230         }
231     }
232 }