cc515f8d3b42c38a325e20c375d88585be4ce628
[netconf.git] / transport / transport-ssh / src / main / java / org / opendaylight / netconf / transport / ssh / SSHTransportStack.java
1 /*
2  * Copyright (c) 2022 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.annotations.VisibleForTesting;
11 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12 import java.io.IOException;
13 import java.util.Collection;
14 import java.util.Map;
15 import java.util.concurrent.ConcurrentHashMap;
16 import java.util.function.Consumer;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.opendaylight.netconf.shaded.sshd.common.FactoryManager;
19 import org.opendaylight.netconf.shaded.sshd.common.SshConstants;
20 import org.opendaylight.netconf.shaded.sshd.common.io.IoHandler;
21 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
22 import org.opendaylight.netconf.shaded.sshd.common.session.SessionListener;
23 import org.opendaylight.netconf.transport.api.AbstractOverlayTransportStack;
24 import org.opendaylight.netconf.transport.api.TransportChannel;
25 import org.opendaylight.netconf.transport.api.TransportChannelListener;
26 import org.opendaylight.netconf.transport.api.TransportStack;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * An SSH {@link TransportStack}. Instances of this class are built indirectly. The setup of the Netty channel is quite
32  * weird. We start off with whatever the underlay sets up.
33  *
34  * <p>
35  * We then add {@link TransportIoSession#getHandler()}, which routes data between the socket and
36  * {@link TransportSshClient} (or {@link TransportSshServer}) -- forming the "bottom half" of the channel.
37  *
38  * <p>
39  * The "upper half" of the channel is formed once the corresponding SSH subsystem is established, via
40  * {@link TransportClientSubsystem}, which installs a {@link OutboundChannelHandler}. These two work together:
41  * <ul>
42  *   <li>TransportClientSubsystem pumps bytes inbound from the subsystem towards the tail of the channel pipeline</li>
43  *   <li>OutboundChannelHandler pumps bytes outbound from the tail of channel pipeline into the subsystem</li>
44  * </ul>
45  */
46 public abstract sealed class SSHTransportStack extends AbstractOverlayTransportStack<SSHTransportChannel>
47         implements SessionListener permits SSHClient, SSHServer {
48     private static final Logger LOG = LoggerFactory.getLogger(SSHTransportStack.class);
49
50     // Underlay TransportChannels which do not have an open subsystem
51     private final Map<Long, TransportChannel> underlays = new ConcurrentHashMap<>();
52     private final Map<Long, Session> sessions = new ConcurrentHashMap<>();
53     private final TransportIoService ioService;
54
55     @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = """
56         SessionListener has default implementations which we do not care about. We have all subclasses in this package
57         and neither of them has additional state""")
58     SSHTransportStack(final TransportChannelListener listener, final FactoryManager factoryManager,
59             final IoHandler handler) {
60         super(listener);
61         ioService = new TransportIoService(factoryManager, handler);
62         factoryManager.addSessionListener(this);
63     }
64
65     @Override
66     protected void onUnderlayChannelEstablished(final TransportChannel underlayChannel) {
67         LOG.debug("Underlay establishing, attaching SSH to {}", underlayChannel);
68         // Acquire underlying channel, create a TransportIoSession and attach its handler to this channel -- which takes
69         // care of routing bytes between the underlay channel and SSHD's network-facing side.
70         final var channel = underlayChannel.channel();
71         final var ioSession = ioService.createSession(channel.localAddress());
72         channel.pipeline().addLast(ioSession.getHandler());
73
74         // we now have an attached underlay, but it needs further processing before we expose it to the end user
75         underlays.put(ioSession.getId(), underlayChannel);
76     }
77
78     // SessionListener integration. Responsible for observing authentication-related events, orchestrating both client
79     // and server interactions.
80     //
81     // The state machine is responsible for driving TransportChannel
82
83     //
84     // At some point we should keep this in an encapsulated state object, but we have specializations, so we keep this
85     // here at the cost of not modeling the solution domain correctly.
86
87     @Override
88     public final void sessionCreated(final Session session) {
89         sessions.put(sessionId(session), session);
90     }
91
92     @Override
93     public final void sessionException(final Session session, final Throwable throwable) {
94         final var sessionId = sessionId(session);
95         LOG.warn("Session {} encountered an error", sessionId, throwable);
96         deleteSession(sessionId);
97     }
98
99     @Override
100     public final void sessionDisconnect(final Session session, final int reason, final String msg,
101             final String language, final boolean initiator) {
102         final var sessionId = sessionId(session);
103         LOG.debug("Session {} disconnected: {}", sessionId, SshConstants.getDisconnectReasonName(reason));
104         deleteSession(sessionId);
105     }
106
107     @Override
108     public final void sessionClosed(final Session session) {
109         final var sessionId = sessionId(session);
110         LOG.debug("Session {} closed", sessionId);
111         deleteSession(sessionId);
112     }
113
114     @Override
115     public final void sessionEvent(final Session session, final Event event) {
116         final var sessionId = sessionId(session);
117         switch (event) {
118             case KeyEstablished -> {
119                 LOG.debug("New key established on session {}", sessionId);
120                 try {
121                     onKeyEstablished(session);
122                 } catch (IOException e) {
123                     LOG.error("Post-key step failed on session {}", sessionId, e);
124                     deleteSession(sessionId);
125                 }
126             }
127             case Authenticated -> {
128                 LOG.debug("Authentication on session {} successful", sessionId);
129                 try {
130                     onAuthenticated(session);
131                 } catch (IOException e) {
132                     LOG.error("Post-authentication step failed on session {}", sessionId, e);
133                     deleteSession(sessionId);
134                 }
135             }
136             case KexCompleted -> {
137                 LOG.debug("Key exchange completed on session {}", sessionId);
138             }
139             default -> {
140                 LOG.debug("Ignoring event {} on session {}", event, sessionId);
141             }
142         }
143     }
144
145     abstract void onKeyEstablished(Session session) throws IOException;
146
147     abstract void onAuthenticated(Session session) throws IOException;
148
149     final @NonNull TransportChannel getUnderlayOf(final Long sessionId) throws IOException {
150         final var ret = underlays.get(sessionId);
151         if (ret == null) {
152             throw new IOException("Cannot find underlay for " + sessionId);
153         }
154         return ret;
155     }
156
157     final void deleteSession(final Long sessionId) {
158         sessions.remove(sessionId);
159         // auth failure, close underlay if any
160         completeUnderlay(sessionId, underlay -> underlay.channel().close());
161     }
162
163     // FIXME: this should be an assertion, the channel should just be there
164     final void transportEstablished(final Long sessionId) {
165         completeUnderlay(sessionId, underlay -> {
166             LOG.debug("Established transport on session {}", sessionId);
167             addTransportChannel(new SSHTransportChannel(underlay));
168         });
169     }
170
171     private void completeUnderlay(final Long sessionId, final Consumer<TransportChannel> action) {
172         final var removed = underlays.remove(sessionId);
173         if (removed != null) {
174             action.accept(removed);
175         }
176     }
177
178     static final Long sessionId(final Session session) {
179         return session.getIoSession().getId();
180     }
181
182     @VisibleForTesting
183     Collection<Session> getSessions() {
184         return sessions.values();
185     }
186 }