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