View Javadoc

1   /*******************************************************************************
2    * Copyright (c) 2007, 2014 Massimiliano Ziccardi
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *******************************************************************************/
16  package it.jnrpe;
17  
18  import io.netty.bootstrap.ServerBootstrap;
19  import io.netty.channel.ChannelFuture;
20  import io.netty.channel.ChannelFutureListener;
21  import io.netty.channel.ChannelInitializer;
22  import io.netty.channel.ChannelOption;
23  import io.netty.channel.EventLoopGroup;
24  import io.netty.channel.nio.NioEventLoopGroup;
25  import io.netty.channel.socket.SocketChannel;
26  import io.netty.channel.socket.nio.NioServerSocketChannel;
27  import io.netty.handler.ssl.SslHandler;
28  import io.netty.handler.timeout.IdleStateHandler;
29  import it.jnrpe.commands.CommandInvoker;
30  import it.jnrpe.commands.CommandRepository;
31  import it.jnrpe.events.EventsUtil;
32  import it.jnrpe.events.IJNRPEEventListener;
33  import it.jnrpe.events.LogEvent;
34  import it.jnrpe.net.JNRPEIdleStateHandler;
35  import it.jnrpe.net.JNRPERequestDecoder;
36  import it.jnrpe.net.JNRPEResponseEncoder;
37  import it.jnrpe.net.JNRPEServerHandler;
38  import it.jnrpe.plugins.IPluginRepository;
39  import it.jnrpe.utils.StreamManager;
40  
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.net.UnknownHostException;
44  import java.nio.charset.Charset;
45  import java.security.KeyManagementException;
46  import java.security.KeyStore;
47  import java.security.KeyStoreException;
48  import java.security.NoSuchAlgorithmException;
49  import java.security.UnrecoverableKeyException;
50  import java.security.cert.CertificateException;
51  import java.util.ArrayList;
52  import java.util.Collection;
53  import java.util.HashSet;
54  
55  import javax.net.ssl.KeyManagerFactory;
56  import javax.net.ssl.SSLContext;
57  import javax.net.ssl.SSLEngine;
58  import javax.net.ssl.SSLException;
59  
60  /**
61   * This class is the real JNRPE worker. It must be used to start listening for
62   * NRPE requests
63   * 
64   * @author Massimiliano Ziccardi
65   */
66  public final class JNRPE {
67  
68  	/**
69  	 * Default number of accepted connections.
70  	 */
71  	static final int DEFAULT_MAX_ACCEPTED_CONNECTIONS = 128;
72  
73  	/**
74  	 * The boss group (see netty documentation).
75  	 */
76  	private final EventLoopGroup bossGroup = new NioEventLoopGroup();
77  
78  	/**
79  	 * The worker group (see netty documentation).
80  	 */
81  	private final EventLoopGroup workerGroup = new NioEventLoopGroup();
82  
83  	/**
84  	 * The default keystore name (used to perform SSL).
85  	 */
86  	private static final String KEYSTORE_NAME = "keys.jks";
87  
88  	/**
89  	 * The default keystore password.
90  	 */
91  	private static final String KEYSTORE_PWD = "p@55w0rd";
92  
93  	/**
94  	 * The plugin repository to be used to find the requested plugin.
95  	 */
96  	private final IPluginRepository pluginRepository;
97  	/**
98  	 * The command repository to be used to find the requested command.
99  	 */
100 	private final CommandRepository commandRepository;
101 
102 	/**
103 	 * The list of accepted clients.
104 	 */
105 	private Collection<String> acceptedHostsList = new ArrayList<String>();
106 
107 	/**
108 	 * All the listeners.
109 	 */
110 	private Collection<IJNRPEEventListener> eventListenersSet = new HashSet<IJNRPEEventListener>();
111 
112 	/**
113 	 * Charset that will be used by JNRPE.
114 	 */
115 	private final Charset charset;
116 
117 	/**
118 	 * The maximum number of concurrent connection allowed by JNRPE.
119 	 */
120 	private final int maxAcceptedConnections;
121 
122 	/**
123 	 * The maximum number of seconds that JNRPE will wait for a client to send a
124 	 * command.
125 	 */
126 	private final int readTimeout;
127 
128 	/**
129 	 * The maximum number of seconds that JNRPE will wait for a plugin to
130 	 * produce a result.
131 	 */
132 	private final int writeTimeout;
133 
134 	/**
135 	 * <code>true</code> if $ARGxx$ macros must be expanded.
136 	 */
137 	private final boolean acceptParams;
138 
139 	/**
140 	 * Instantiates the JNRPE engine.
141 	 * 
142 	 * @param pluginRepo
143 	 *            The plugin repository object
144 	 * @param commandRepo
145 	 *            The command repository object
146 	 * 
147 	 * @deprecated This constructor will be removed as of version 2.0.5. Use
148 	 *             {@link JNRPEBuilder} instead
149 	 */
150 	@Deprecated
151 	public JNRPE(final IPluginRepository pluginRepo,
152 			final CommandRepository commandRepo) {
153 		if (pluginRepo == null) {
154 			throw new IllegalArgumentException(
155 					"Plugin repository cannot be null");
156 		}
157 
158 		if (commandRepo == null) {
159 			throw new IllegalArgumentException(
160 					"Command repository cannot be null");
161 		}
162 		pluginRepository = pluginRepo;
163 		commandRepository = commandRepo;
164 		charset = Charset.forName("UTF-8");
165 		this.acceptParams = true;
166 		this.maxAcceptedConnections = DEFAULT_MAX_ACCEPTED_CONNECTIONS;
167 		readTimeout = 10;
168 		writeTimeout = 60;
169 	}
170 
171 	/**
172 	 * Initializes the JNRPE worker.
173 	 * 
174 	 * @param pluginRepo
175 	 *            The repository containing all the installed plugins
176 	 * @param commandRepo
177 	 *            The repository containing all the configured commands.
178 	 * @param newCharset
179 	 *            The charset that will be used by JNRPE
180 	 * @param acceptParameters
181 	 *            Sets if $ARGxx$ macros should be expanded
182 	 * @deprecated This constructor will be removed as of version 2.0.5. Use
183 	 *             {@link JNRPEBuilder} instead
184 	 */
185 	@Deprecated
186 	public JNRPE(final IPluginRepository pluginRepo,
187 			final CommandRepository commandRepo, final Charset newCharset,
188 			final boolean acceptParameters) {
189 		if (pluginRepo == null) {
190 			throw new IllegalArgumentException(
191 					"Plugin repository cannot be null");
192 		}
193 
194 		if (commandRepo == null) {
195 			throw new IllegalArgumentException(
196 					"Command repository cannot be null");
197 		}
198 		pluginRepository = pluginRepo;
199 		commandRepository = commandRepo;
200 		this.charset = newCharset;
201 		this.acceptParams = acceptParameters;
202 		this.maxAcceptedConnections = DEFAULT_MAX_ACCEPTED_CONNECTIONS;
203 		readTimeout = 10;
204 		writeTimeout = 60;
205 	}
206 
207 	/**
208 	 * Constructor used by the {@link JNRPEBuilder} to build an immutable
209 	 * instance of {@link JNRPE}.
210 	 * 
211 	 * @param pluginRepo
212 	 *            The plugin repository object
213 	 * @param commandRepo
214 	 *            The command repository object
215 	 * @param newCharset
216 	 *            The charset that JNRPE will use
217 	 * @param acceptParameters
218 	 *            Sets if $ARGxx$ macros should be expanded
219 	 * @param acceptedHostsCollection
220 	 *            The list of accepted client hosts
221 	 * @param maxConnections
222 	 *            The maximum number of concurrent connections
223 	 * @param readTimeoutSeconds
224 	 *            The maximum number of seconds to wait for the client to send
225 	 *            the command
226 	 * @param writeTimeoutSeconds
227 	 *            The maximum number of seconds to wait for a plugin to return a
228 	 *            result
229 	 * @param eventListeners
230 	 *            The collection of listeners that will receive JNRPE events
231 	 */
232 	JNRPE(final IPluginRepository pluginRepo,
233 			final CommandRepository commandRepo, final Charset newCharset,
234 			final boolean acceptParameters,
235 			final Collection<String> acceptedHostsCollection,
236 			final int maxConnections, final int readTimeoutSeconds,
237 			final int writeTimeoutSeconds,
238 			final Collection<IJNRPEEventListener> eventListeners) {
239 		if (pluginRepo == null) {
240 			throw new IllegalArgumentException(
241 					"Plugin repository cannot be null");
242 		}
243 
244 		if (commandRepo == null) {
245 			throw new IllegalArgumentException(
246 					"Command repository cannot be null");
247 		}
248 		pluginRepository = pluginRepo;
249 		commandRepository = commandRepo;
250 		this.charset = newCharset;
251 		this.acceptParams = acceptParameters;
252 		this.acceptedHostsList = acceptedHostsCollection;
253 		this.eventListenersSet = eventListeners;
254 		this.maxAcceptedConnections = maxConnections;
255 		this.readTimeout = readTimeoutSeconds;
256 		this.writeTimeout = writeTimeoutSeconds;
257 
258 	}
259 
260 	/**
261 	 * Instructs the server to listen to the given IP/port.
262 	 * 
263 	 * @param address
264 	 *            The address to bind to
265 	 * @param port
266 	 *            The port to bind to
267 	 * @throws UnknownHostException
268 	 *             -
269 	 */
270 	public void listen(final String address, final int port)
271 			throws UnknownHostException {
272 		listen(address, port, true);
273 	}
274 
275 	/**
276 	 * Adds a new event listener.
277 	 * 
278 	 * @param listener
279 	 *            The event listener to be added
280 	 * 
281 	 * @deprecated The JNRPE object will become immutable as of version 2.0.5.
282 	 *             Use {@link JNRPEBuilder} instead
283 	 */
284 	@Deprecated
285 	public void addEventListener(final IJNRPEEventListener listener) {
286 		eventListenersSet.add(listener);
287 	}
288 
289 	/**
290 	 * Creates, configures and returns the SSL engine.
291 	 * 
292 	 * @return the SSL Engine
293 	 * @throws KeyStoreException
294 	 * @throws CertificateException
295 	 * @throws IOException
296 	 * @throws UnrecoverableKeyException
297 	 * @throws KeyManagementException
298 	 */
299 	private SSLEngine getSSLEngine() throws KeyStoreException,
300 			CertificateException, IOException, UnrecoverableKeyException,
301 			KeyManagementException {
302 
303 		// Open the KeyStore Stream
304 		StreamManager h = new StreamManager();
305 
306 		SSLContext ctx;
307 		KeyManagerFactory kmf;
308 
309 		try {
310 			InputStream ksStream = getClass().getClassLoader()
311 					.getResourceAsStream(KEYSTORE_NAME);
312 			h.handle(ksStream);
313 			ctx = SSLContext.getInstance("SSLv3");
314 
315 			kmf = KeyManagerFactory.getInstance(KeyManagerFactory
316 					.getDefaultAlgorithm());
317 
318 			KeyStore ks = KeyStore.getInstance("JKS");
319 			char[] passphrase = KEYSTORE_PWD.toCharArray();
320 			ks.load(ksStream, passphrase);
321 
322 			kmf.init(ks, passphrase);
323 			ctx.init(kmf.getKeyManagers(), null,
324 					new java.security.SecureRandom());
325 		} catch (NoSuchAlgorithmException e) {
326 			throw new SSLException("Unable to initialize SSLSocketFactory.\n"
327 					+ e.getMessage());
328 		} finally {
329 			h.closeAll();
330 		}
331 
332 		return ctx.createSSLEngine();
333 	}
334 
335 	/**
336 	 * Creates and returns a configured NETTY ServerBootstrap object.
337 	 * 
338 	 * @param useSSL
339 	 *            <code>true</code> if SSL must be used.
340 	 * @return the server bootstrap object
341 	 */
342 	private ServerBootstrap getServerBootstrap(final boolean useSSL) {
343 
344 		final CommandInvoker invoker = new CommandInvoker(pluginRepository,
345 				commandRepository, acceptParams, eventListenersSet);
346 
347 		ServerBootstrap b = new ServerBootstrap();
348 		b.group(bossGroup, workerGroup)
349 				.channel(NioServerSocketChannel.class)
350 				.childHandler(new ChannelInitializer<SocketChannel>() {
351 					@Override
352 					public void initChannel(final SocketChannel ch)
353 							throws Exception {
354 
355 						if (useSSL) {
356 							SSLEngine engine = getSSLEngine();
357 							engine.setEnabledCipherSuites(engine
358 									.getSupportedCipherSuites());
359 							engine.setUseClientMode(false);
360 							engine.setNeedClientAuth(false);
361 							ch.pipeline()
362 									.addLast("ssl", new SslHandler(engine));
363 						}
364 
365 						ch.pipeline()
366 								.addLast(
367 										new JNRPERequestDecoder(),
368 										new JNRPEResponseEncoder(),
369 										new JNRPEServerHandler(invoker,
370 												eventListenersSet))
371 								.addLast(
372 										"idleStateHandler",
373 										new IdleStateHandler(readTimeout,
374 												writeTimeout, 0))
375 								.addLast(
376 										"jnrpeIdleStateHandler",
377 										new JNRPEIdleStateHandler(
378 												new JNRPEExecutionContext(
379 														JNRPE.this.eventListenersSet,
380 														Charset.defaultCharset())));
381 					}
382 				})
383 				.option(ChannelOption.SO_BACKLOG, this.maxAcceptedConnections)
384 				.childOption(ChannelOption.SO_KEEPALIVE, true);
385 
386 		return b;
387 	}
388 
389 	/**
390 	 * Starts a new thread that listen for requests. The method is <b>not
391 	 * blocking</b>
392 	 * 
393 	 * @param address
394 	 *            The address to bind to
395 	 * @param port
396 	 *            The listening port
397 	 * @param useSSL
398 	 *            <code>true</code> if an SSL socket must be created.
399 	 * @throws UnknownHostException
400 	 *             -
401 	 */
402 	public void listen(final String address, final int port,
403 			final boolean useSSL) throws UnknownHostException {
404 
405 		// Bind and start to accept incoming connections.
406 		ChannelFuture cf = getServerBootstrap(useSSL).bind(address, port);
407 		cf.addListener(new ChannelFutureListener() {
408 
409 			public void operationComplete(final ChannelFuture future)
410 					throws Exception {
411 				if (future.isSuccess()) {
412 					EventsUtil.sendEvent(eventListenersSet, this,
413 							LogEvent.INFO, "Listening on "
414 									+ (useSSL ? "SSL/" : "") + address + ":"
415 									+ port);
416 				} else {
417 					EventsUtil.sendEvent(eventListenersSet, this,
418 							LogEvent.ERROR, "Unable to listen on "
419 									+ (useSSL ? "SSL/" : "") + address + ":"
420 									+ port, future.cause());
421 				}
422 			}
423 		});
424 
425 	}
426 
427 	/**
428 	 * Adds an address to the list of accepted hosts.
429 	 * 
430 	 * @param address
431 	 *            The address to accept
432 	 * @deprecated The JNRPE object will become immutable as of version 2.0.5.
433 	 *             Use {@link JNRPEBuilder} instead
434 	 */
435 	@Deprecated
436 	public void addAcceptedHost(final String address) {
437 		acceptedHostsList.add(address);
438 	}
439 
440 	/**
441 	 * Shuts down the server.
442 	 */
443 	public void shutdown() {
444 		workerGroup.shutdownGracefully().syncUninterruptibly();
445 		bossGroup.shutdownGracefully().syncUninterruptibly();
446 	}
447 }