Require a subsystem for client connections
[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.Nullable;
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 @Nullable TransportChannel underlayOf(final Long sessionId) {
150         return underlays.get(sessionId);
151     }
152
153     final void deleteSession(final Long sessionId) {
154         sessions.remove(sessionId);
155         // auth failure, close underlay if any
156         completeUnderlay(sessionId, underlay -> underlay.channel().close());
157     }
158
159     final void completeUnderlay(final Long sessionId, final Consumer<TransportChannel> action) {
160         final var removed = underlays.remove(sessionId);
161         if (removed != null) {
162             action.accept(removed);
163         }
164     }
165
166     static final Long sessionId(final Session session) {
167         return session.getIoSession().getId();
168     }
169
170     @VisibleForTesting
171     Collection<Session> getSessions() {
172         return sessions.values();
173     }
174 }