Fix periodic NETCONF Call Home connection dropping
[netconf.git] / netconf / callhome-protocol / src / main / java / org / opendaylight / netconf / callhome / protocol / NetconfCallHomeServer.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 java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import java.io.IOException;
14 import java.net.InetSocketAddress;
15 import java.net.SocketAddress;
16 import java.security.PublicKey;
17 import org.opendaylight.netconf.callhome.protocol.CallHomeSessionContext.Factory;
18 import org.opendaylight.netconf.shaded.sshd.client.SshClient;
19 import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture;
20 import org.opendaylight.netconf.shaded.sshd.client.keyverifier.ServerKeyVerifier;
21 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
22 import org.opendaylight.netconf.shaded.sshd.client.session.SessionFactory;
23 import org.opendaylight.netconf.shaded.sshd.common.future.SshFutureListener;
24 import org.opendaylight.netconf.shaded.sshd.common.io.IoAcceptor;
25 import org.opendaylight.netconf.shaded.sshd.common.io.IoServiceFactory;
26 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
27 import org.opendaylight.netconf.shaded.sshd.common.session.SessionListener;
28 import org.opendaylight.netconf.shaded.sshd.netty.NettyIoServiceFactory;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 public final class NetconfCallHomeServer implements AutoCloseable, ServerKeyVerifier {
33     private static final Logger LOG = LoggerFactory.getLogger(NetconfCallHomeServer.class);
34
35     private final CallHomeAuthorizationProvider authProvider;
36     private final IoServiceFactory serviceFactory;
37     private final InetSocketAddress bindAddress;
38     private final StatusRecorder recorder;
39     private final Factory sessionFactory;
40     private final IoAcceptor acceptor;
41     private final SshClient client;
42
43     NetconfCallHomeServer(final SshClient sshClient, final CallHomeAuthorizationProvider authProvider,
44             final Factory factory, final InetSocketAddress socketAddress, final StatusRecorder recorder) {
45         this(sshClient, authProvider, factory, socketAddress, recorder,
46             new NettyIoServiceFactory(factory.getNettyGroup()));
47     }
48
49     @VisibleForTesting
50     NetconfCallHomeServer(final SshClient sshClient, final CallHomeAuthorizationProvider authProvider,
51             final Factory factory, final InetSocketAddress socketAddress, final StatusRecorder recorder,
52             final IoServiceFactory serviceFactory) {
53         client = requireNonNull(sshClient);
54         this.authProvider = requireNonNull(authProvider);
55         sessionFactory = requireNonNull(factory);
56         bindAddress = socketAddress;
57         this.recorder = recorder;
58         this.serviceFactory = requireNonNull(serviceFactory);
59
60         sshClient.setServerKeyVerifier(this);
61         sshClient.addSessionListener(createSessionListener());
62
63         acceptor = serviceFactory.createAcceptor(new SessionFactory(sshClient));
64     }
65
66     @VisibleForTesting
67     SshClient getClient() {
68         return client;
69     }
70
71     SessionListener createSessionListener() {
72         return new SessionListener() {
73             @Override
74             public void sessionEvent(final Session session, final Event event) {
75                 ClientSession clientSession = (ClientSession) session;
76                 LOG.debug("SSH session {} event {}", session, event);
77                 switch (event) {
78                     case KeyEstablished:
79                         // Case of key re-exchange - if session is once authenticated, it does not need to be made again
80                         if (!clientSession.isAuthenticated()) {
81                             doAuth(clientSession);
82                         }
83                         break;
84                     case Authenticated:
85                         CallHomeSessionContext.getFrom(clientSession).openNetconfChannel();
86                         break;
87                     default:
88                         break;
89                 }
90             }
91
92             @Override
93             public void sessionCreated(final Session session) {
94                 LOG.debug("SSH session {} created", session);
95             }
96
97             @Override
98             public void sessionClosed(final Session session) {
99                 CallHomeSessionContext ctx = CallHomeSessionContext.getFrom((ClientSession) session);
100                 if (ctx != null) {
101                     ctx.removeSelf();
102                 }
103                 LOG.debug("SSH Session {} closed", session);
104             }
105
106             private void doAuth(final ClientSession session) {
107                 try {
108                     final AuthFuture authFuture = CallHomeSessionContext.getFrom(session).authorize();
109                     authFuture.addListener(newAuthSshFutureListener(session));
110                 } catch (IOException e) {
111                     LOG.error("Failed to authorize session {}", session, e);
112                 }
113             }
114         };
115     }
116
117     private SshFutureListener<AuthFuture> newAuthSshFutureListener(final ClientSession session) {
118         final PublicKey serverKey = session.getServerKey();
119
120         return new SshFutureListener<>() {
121             @Override
122             public void operationComplete(final AuthFuture authFuture) {
123                 if (authFuture.isSuccess()) {
124                     onSuccess();
125                 } else if (authFuture.isFailure()) {
126                     onFailure(authFuture.getException());
127                 } else if (authFuture.isCanceled()) {
128                     onCanceled();
129                 }
130                 authFuture.removeListener(this);
131             }
132
133             private void onSuccess() {
134                 LOG.debug("Authorize success");
135             }
136
137             private void onFailure(final Throwable throwable) {
138                 LOG.error("Authorize failed for session {}", session, throwable);
139                 recorder.reportFailedAuth(serverKey);
140                 session.close(true);
141             }
142
143             private void onCanceled() {
144                 LOG.warn("Authorize cancelled");
145                 session.close(true);
146             }
147         };
148     }
149
150     @Override
151     public boolean verifyServerKey(final ClientSession sshClientSession, final SocketAddress remoteAddress,
152             final PublicKey serverKey) {
153         final CallHomeAuthorization authorization = authProvider.provideAuth(remoteAddress, serverKey);
154         if (!authorization.isServerAllowed()) {
155             // server is not authorized
156             LOG.info("Incoming session {} was rejected by Authorization Provider.", sshClientSession);
157             return false;
158         }
159
160         if (sessionFactory.createIfNotExists(sshClientSession, authorization, remoteAddress) == null) {
161             // Session was not created, session with same name exists
162             LOG.info("Incoming session {} was rejected. Session with same name {} is already active.", sshClientSession,
163                 authorization.getSessionName());
164             return false;
165         }
166
167         // Session was created, session with same name does not exist
168         LOG.debug("Incoming session {} was successfully verified.", sshClientSession);
169         return true;
170     }
171
172     public void bind() throws IOException {
173         try {
174             client.start();
175             acceptor.bind(bindAddress);
176         } catch (IOException e) {
177             LOG.error("Unable to start NETCONF CallHome Service on {}", bindAddress, e);
178             throw e;
179         }
180     }
181
182     @Override
183     public void close() {
184         acceptor.close(true);
185         serviceFactory.close(true);
186     }
187 }