83a62f4fcdf9f9c2c3de563665a705c7aeb3c0c4
[netconf.git] / netconf / callhome-protocol / src / main / java / org / opendaylight / netconf / callhome / protocol / CallHomeSessionContext.java
1 /*
2  * Copyright (c) 2016 Brocade Communication Systems 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.callhome.protocol;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.annotations.VisibleForTesting;
14 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
15 import io.netty.channel.EventLoopGroup;
16 import io.netty.util.concurrent.GlobalEventExecutor;
17 import io.netty.util.concurrent.Promise;
18 import java.io.IOException;
19 import java.net.InetSocketAddress;
20 import java.net.SocketAddress;
21 import java.security.PublicKey;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ConcurrentMap;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.opendaylight.netconf.client.NetconfClientSession;
26 import org.opendaylight.netconf.client.NetconfClientSessionListener;
27 import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
28 import org.opendaylight.netconf.shaded.sshd.client.channel.ClientChannel;
29 import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture;
30 import org.opendaylight.netconf.shaded.sshd.client.future.OpenFuture;
31 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
32 import org.opendaylight.netconf.shaded.sshd.common.future.SshFutureListener;
33 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 // Non-final for testing
38 class CallHomeSessionContext implements CallHomeProtocolSessionContext {
39
40     private static final Logger LOG = LoggerFactory.getLogger(CallHomeSessionContext.class);
41     private static final String NETCONF = "netconf";
42
43     @VisibleForTesting
44     static final Session.AttributeKey<CallHomeSessionContext> SESSION_KEY = new Session.AttributeKey<>();
45
46     private final ClientSession sshSession;
47     private final CallHomeAuthorization authorization;
48     private final Factory factory;
49
50     private volatile boolean activated;
51
52     private final InetSocketAddress remoteAddress;
53     private final PublicKey serverKey;
54
55     @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = "Passing 'this' around")
56     CallHomeSessionContext(final ClientSession sshSession, final CallHomeAuthorization authorization,
57                            final SocketAddress remoteAddress, final Factory factory) {
58         this.authorization = requireNonNull(authorization, "authorization");
59         checkArgument(this.authorization.isServerAllowed(), "Server was not allowed.");
60         this.factory = requireNonNull(factory);
61         this.sshSession = requireNonNull(sshSession);
62         this.remoteAddress = (InetSocketAddress) this.sshSession.getIoSession().getRemoteAddress();
63         serverKey = this.sshSession.getServerKey();
64     }
65
66     final void associate() {
67         sshSession.setAttribute(SESSION_KEY, this);
68     }
69
70     static CallHomeSessionContext getFrom(final ClientSession sshSession) {
71         return sshSession.getAttribute(SESSION_KEY);
72     }
73
74     AuthFuture authorize() throws IOException {
75         authorization.applyTo(sshSession);
76         return sshSession.auth();
77     }
78
79     void openNetconfChannel() {
80         LOG.debug("Opening NETCONF Subsystem on {}", sshSession);
81         try {
82             final ClientChannel netconfChannel = sshSession.createSubsystemChannel(NETCONF);
83             netconfChannel.setStreaming(ClientChannel.Streaming.Async);
84             netconfChannel.open().addListener(newSshFutureListener(netconfChannel));
85         } catch (IOException e) {
86             throw new IllegalStateException(e);
87         }
88     }
89
90     SshFutureListener<OpenFuture> newSshFutureListener(final ClientChannel netconfChannel) {
91         return future -> {
92             if (future.isOpened()) {
93                 factory.getChannelOpenListener().onNetconfSubsystemOpened(this,
94                     listener -> doActivate(netconfChannel, listener));
95             } else {
96                 channelOpenFailed(future.getException());
97             }
98         };
99     }
100
101     @Override
102     public void terminate() {
103         sshSession.close(false);
104         removeSelf();
105     }
106
107     @Override
108     public TransportType getTransportType() {
109         return TransportType.SSH;
110     }
111
112     private void channelOpenFailed(final Throwable throwable) {
113         LOG.error("Unable to open netconf subsystem, disconnecting.", throwable);
114         sshSession.close(false);
115     }
116
117     private synchronized Promise<NetconfClientSession> doActivate(final ClientChannel netconfChannel,
118             final NetconfClientSessionListener listener) {
119         if (activated) {
120             return newSessionPromise().setFailure(new IllegalStateException("Session already activated."));
121         }
122
123         activated = true;
124         LOG.info("Activating Netconf channel for {} with {}", getRemoteAddress(), listener);
125         Promise<NetconfClientSession> activationPromise = newSessionPromise();
126         final MinaSshNettyChannel nettyChannel = newMinaSshNettyChannel(netconfChannel);
127         factory.getChannelInitializer(listener).initialize(nettyChannel, activationPromise);
128         factory.getNettyGroup().register(nettyChannel).awaitUninterruptibly(500);
129         return activationPromise;
130     }
131
132     protected MinaSshNettyChannel newMinaSshNettyChannel(final ClientChannel netconfChannel) {
133         return new MinaSshNettyChannel(this, sshSession, netconfChannel);
134     }
135
136     private static Promise<NetconfClientSession> newSessionPromise() {
137         return GlobalEventExecutor.INSTANCE.newPromise();
138     }
139
140     @Override
141     public PublicKey getRemoteServerKey() {
142         return serverKey;
143     }
144
145     @Override
146     public InetSocketAddress getRemoteAddress() {
147         return remoteAddress;
148     }
149
150     @Override
151     public String getSessionId() {
152         return authorization.getSessionName();
153     }
154
155     void removeSelf() {
156         factory.remove(this);
157     }
158
159     static class Factory {
160         private final ConcurrentMap<String, CallHomeSessionContext> sessions = new ConcurrentHashMap<>();
161         private final EventLoopGroup nettyGroup;
162         private final NetconfClientSessionNegotiatorFactory negotiatorFactory;
163         private final CallHomeNetconfSubsystemListener subsystemListener;
164
165         Factory(final EventLoopGroup nettyGroup, final NetconfClientSessionNegotiatorFactory negotiatorFactory,
166                 final CallHomeNetconfSubsystemListener subsystemListener) {
167             this.nettyGroup = requireNonNull(nettyGroup);
168             this.negotiatorFactory = requireNonNull(negotiatorFactory);
169             this.subsystemListener = requireNonNull(subsystemListener);
170         }
171
172         ReverseSshChannelInitializer getChannelInitializer(final NetconfClientSessionListener listener) {
173             return ReverseSshChannelInitializer.create(negotiatorFactory, listener);
174         }
175
176         CallHomeNetconfSubsystemListener getChannelOpenListener() {
177             return subsystemListener;
178         }
179
180         EventLoopGroup getNettyGroup() {
181             return nettyGroup;
182         }
183
184         @Nullable CallHomeSessionContext createIfNotExists(final ClientSession sshSession,
185                 final CallHomeAuthorization authorization, final SocketAddress remoteAddress) {
186             final var newSession = new CallHomeSessionContext(sshSession, authorization, remoteAddress, this);
187             final var existing = sessions.putIfAbsent(newSession.getSessionId(), newSession);
188             if (existing == null) {
189                 // There was no mapping, but now there is. Associate the the context with the session.
190                 newSession.associate();
191                 return newSession;
192             }
193
194             // We already have a mapping, do not create a new one. But also check if the current session matches
195             // the one stored in the session. This can happen during rekeying.
196             return existing == CallHomeSessionContext.getFrom(sshSession) ? existing : null;
197         }
198
199         void remove(final CallHomeSessionContext session) {
200             sessions.remove(session.getSessionId(), session);
201         }
202     }
203 }