beb9282ba97a5aab0e208548cf14c6d3c870d1e7
[netconf.git] / protocol / netconf-client / src / main / java / org / opendaylight / netconf / client / NetconfClientSessionNegotiator.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. 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.client;
9
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.base.Strings;
12 import com.google.common.collect.ImmutableSet;
13 import com.google.common.collect.Interner;
14 import com.google.common.collect.Interners;
15 import io.netty.channel.Channel;
16 import io.netty.channel.ChannelHandlerContext;
17 import io.netty.channel.ChannelInboundHandlerAdapter;
18 import io.netty.util.Timer;
19 import io.netty.util.concurrent.Promise;
20 import java.util.Set;
21 import javax.xml.xpath.XPathConstants;
22 import javax.xml.xpath.XPathExpression;
23 import javax.xml.xpath.XPathExpressionException;
24 import org.checkerframework.checker.index.qual.NonNegative;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.opendaylight.netconf.api.NetconfDocumentedException;
27 import org.opendaylight.netconf.api.messages.HelloMessage;
28 import org.opendaylight.netconf.api.messages.NetconfMessage;
29 import org.opendaylight.netconf.api.messages.RpcMessage;
30 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
31 import org.opendaylight.netconf.api.xml.XmlUtil;
32 import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
33 import org.opendaylight.netconf.nettyutil.NetconfSessionNegotiator;
34 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.SessionIdType;
35 import org.opendaylight.yangtools.yang.common.Uint32;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.w3c.dom.Document;
39 import org.w3c.dom.Node;
40 import org.w3c.dom.NodeList;
41
42 // Non-final for mocking
43 class NetconfClientSessionNegotiator
44         extends NetconfSessionNegotiator<NetconfClientSession, NetconfClientSessionListener> {
45     private static final Logger LOG = LoggerFactory.getLogger(NetconfClientSessionNegotiator.class);
46
47     private static final XPathExpression SESSION_ID_X_PATH = XMLNetconfUtil
48             .compileXPath("/netconf:hello/netconf:session-id");
49
50     private static final XPathExpression SESSION_ID_X_PATH_NO_NAMESPACE = XMLNetconfUtil
51             .compileXPath("/hello/session-id");
52
53     private static final String EXI_1_0_CAPABILITY_MARKER = "exi:1.0";
54
55     private static final Interner<Set<String>> INTERNER = Interners.newWeakInterner();
56
57     private final RpcMessage startExi;
58
59     NetconfClientSessionNegotiator(final HelloMessage hello, final RpcMessage startExi,
60             final Promise<NetconfClientSession> promise, final Channel channel, final Timer timer,
61             final NetconfClientSessionListener sessionListener, final long connectionTimeoutMillis,
62             final @NonNegative int maximumIncomingChunkSize) {
63         super(hello, promise, channel, timer, sessionListener, connectionTimeoutMillis, maximumIncomingChunkSize);
64         this.startExi = startExi;
65     }
66
67     @SuppressWarnings("checkstyle:IllegalCatch")
68     @Override
69     protected void handleMessage(final HelloMessage netconfMessage) throws NetconfDocumentedException {
70         if (!ifNegotiatedAlready()) {
71             LOG.debug("Server hello message received, starting negotiation on channel {}", channel);
72             try {
73                 startNegotiation();
74             } catch (final Exception e) {
75                 LOG.warn("Unexpected negotiation failure on channel {}", channel, e);
76                 negotiationFailed(e);
77                 return;
78             }
79         }
80         final NetconfClientSession session = getSessionForHelloMessage(netconfMessage);
81         replaceHelloMessageInboundHandler(session);
82
83         // If exi should be used, try to initiate exi communication
84         // Call negotiationSuccessFul after exi negotiation is finished successfully or not
85         if (startExi != null && shouldUseExi(netconfMessage)) {
86             LOG.debug("Netconf session {} should use exi.", session);
87             tryToInitiateExi(session, startExi);
88         } else {
89             // Exi is not supported, release session immediately
90             LOG.debug("Netconf session {} isn't capable of using exi.", session);
91             negotiationSuccessful(session);
92         }
93     }
94
95     /**
96      * Initiates exi communication by sending start-exi message and waiting for positive/negative response.
97      *
98      * @param startExiMessage Exi message for initilization of exi communication.
99      */
100     void tryToInitiateExi(final NetconfClientSession session, final RpcMessage startExiMessage) {
101         channel.pipeline().addAfter(AbstractChannelInitializer.NETCONF_MESSAGE_DECODER,
102                 ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER,
103                 new ExiConfirmationInboundHandler(session, startExiMessage));
104
105         session.sendMessage(startExiMessage).addListener(channelFuture -> {
106             if (!channelFuture.isSuccess()) {
107                 LOG.warn("Failed to send start-exi message {} on session {}", startExiMessage, session,
108                         channelFuture.cause());
109                 channel.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
110             } else {
111                 LOG.trace("Start-exi message {} sent to socket on session {}", startExiMessage, session);
112             }
113         });
114     }
115
116     private boolean shouldUseExi(final HelloMessage helloMsg) {
117         return containsExi10Capability(helloMsg.getDocument()) && containsExi10Capability(localHello().getDocument());
118     }
119
120     private static boolean containsExi10Capability(final Document doc) {
121         final NodeList nList = doc.getElementsByTagName(XmlNetconfConstants.CAPABILITY);
122         for (int i = 0; i < nList.getLength(); i++) {
123             if (nList.item(i).getTextContent().contains(EXI_1_0_CAPABILITY_MARKER)) {
124                 return true;
125             }
126         }
127         return false;
128     }
129
130     private static @NonNull SessionIdType extractSessionId(final Document doc) {
131         String textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH);
132         if (Strings.isNullOrEmpty(textContent)) {
133             textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH_NO_NAMESPACE);
134             if (Strings.isNullOrEmpty(textContent)) {
135                 throw new IllegalStateException("Session id not received from server, hello message: " + XmlUtil
136                         .toString(doc));
137             }
138         }
139         return new SessionIdType(Uint32.valueOf(textContent));
140     }
141
142     private static String getSessionIdWithXPath(final Document doc, final XPathExpression sessionIdXPath) {
143         final var sessionIdNode = evaluateXPath(sessionIdXPath, doc);
144         return sessionIdNode != null ? sessionIdNode.getTextContent() : null;
145     }
146
147     @Override
148     protected NetconfClientSession getSession(final NetconfClientSessionListener sessionListener, final Channel channel,
149                                               final HelloMessage message) {
150         final var sessionId = extractSessionId(message.getDocument());
151
152         // Copy here is important: it disconnects the strings from the document
153         final var capabilities = INTERNER.intern(ImmutableSet.copyOf(
154             NetconfMessageUtil.extractCapabilitiesFromHello(message .getDocument())));
155
156         return new NetconfClientSession(sessionListener, channel, sessionId, capabilities);
157     }
158
159     @VisibleForTesting
160     static Node evaluateXPath(final XPathExpression expr, final Object rootNode) {
161         try {
162             return (Node) expr.evaluate(rootNode, XPathConstants.NODE);
163         } catch (final XPathExpressionException e) {
164             throw new IllegalStateException("Error while evaluating xpath expression " + expr, e);
165         }
166     }
167
168     /**
169      * Handler to process response for start-exi message.
170      */
171     private final class ExiConfirmationInboundHandler extends ChannelInboundHandlerAdapter {
172         private static final String EXI_CONFIRMED_HANDLER = "exiConfirmedHandler";
173
174         private final NetconfClientSession session;
175         private final RpcMessage startExiMessage;
176
177         ExiConfirmationInboundHandler(final NetconfClientSession session,
178                                       final RpcMessage startExiMessage) {
179             this.session = session;
180             this.startExiMessage = startExiMessage;
181         }
182
183         @SuppressWarnings("checkstyle:IllegalCatch")
184         @Override
185         public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
186             ctx.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
187
188             NetconfMessage netconfMessage = (NetconfMessage) msg;
189
190             // Ok response to start-exi, try to add exi handlers
191             if (NetconfMessageUtil.isOKMessage(netconfMessage)) {
192                 LOG.trace("Positive response on start-exi call received on session {}", session);
193                 try {
194                     session.startExiCommunication(startExiMessage);
195                 } catch (RuntimeException e) {
196                     // Unable to add exi, continue without exi
197                     LOG.warn("Unable to start exi communication, Communication will continue without exi on session {}",
198                         session, e);
199                 }
200
201                 // Error response
202             } else if (NetconfMessageUtil.isErrorMessage(netconfMessage)) {
203                 LOG.warn(
204                         "Error response to start-exi message {}, Communication will continue without exi on session {}",
205                         netconfMessage, session);
206
207                 // Unexpected response to start-exi, throwing message away, continue without exi
208             } else {
209                 LOG.warn("Unexpected response to start-exi message, should be ok, was {}, "
210                         + "Communication will continue without exi "
211                         + "and response message will be thrown away on session {}",
212                         netconfMessage, session);
213             }
214
215             negotiationSuccessful(session);
216         }
217     }
218
219 }