2 * Copyright (c) 2016 Brocade Communication Systems and others. All rights reserved.
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
8 package org.opendaylight.netconf.callhome.protocol;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
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;
37 // Non-final for testing
38 class CallHomeSessionContext implements CallHomeProtocolSessionContext {
40 private static final Logger LOG = LoggerFactory.getLogger(CallHomeSessionContext.class);
41 private static final String NETCONF = "netconf";
44 static final Session.AttributeKey<CallHomeSessionContext> SESSION_KEY = new Session.AttributeKey<>();
46 private final ClientSession sshSession;
47 private final CallHomeAuthorization authorization;
48 private final Factory factory;
50 private volatile boolean activated;
52 private final InetSocketAddress remoteAddress;
53 private final PublicKey serverKey;
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();
66 final void associate() {
67 sshSession.setAttribute(SESSION_KEY, this);
70 static CallHomeSessionContext getFrom(final ClientSession sshSession) {
71 return sshSession.getAttribute(SESSION_KEY);
74 AuthFuture authorize() throws IOException {
75 authorization.applyTo(sshSession);
76 return sshSession.auth();
79 void openNetconfChannel() {
80 LOG.debug("Opening NETCONF Subsystem on {}", sshSession);
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);
90 SshFutureListener<OpenFuture> newSshFutureListener(final ClientChannel netconfChannel) {
92 if (future.isOpened()) {
93 factory.getChannelOpenListener().onNetconfSubsystemOpened(this,
94 listener -> doActivate(netconfChannel, listener));
96 channelOpenFailed(future.getException());
102 public void terminate() {
103 sshSession.close(false);
108 public TransportType getTransportType() {
109 return TransportType.SSH;
112 private void channelOpenFailed(final Throwable throwable) {
113 LOG.error("Unable to open netconf subsystem, disconnecting.", throwable);
114 sshSession.close(false);
117 private synchronized Promise<NetconfClientSession> doActivate(final ClientChannel netconfChannel,
118 final NetconfClientSessionListener listener) {
120 return newSessionPromise().setFailure(new IllegalStateException("Session already activated."));
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;
132 protected MinaSshNettyChannel newMinaSshNettyChannel(final ClientChannel netconfChannel) {
133 return new MinaSshNettyChannel(this, sshSession, netconfChannel);
136 private static Promise<NetconfClientSession> newSessionPromise() {
137 return GlobalEventExecutor.INSTANCE.newPromise();
141 public PublicKey getRemoteServerKey() {
146 public InetSocketAddress getRemoteAddress() {
147 return remoteAddress;
151 public String getSessionId() {
152 return authorization.getSessionName();
156 factory.remove(this);
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;
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);
172 ReverseSshChannelInitializer getChannelInitializer(final NetconfClientSessionListener listener) {
173 return ReverseSshChannelInitializer.create(negotiatorFactory, listener);
176 CallHomeNetconfSubsystemListener getChannelOpenListener() {
177 return subsystemListener;
180 EventLoopGroup getNettyGroup() {
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();
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;
199 void remove(final CallHomeSessionContext session) {
200 sessions.remove(session.getSessionId(), session);