From 9ab97140641bc964ba70e07ff5f339446501ceed Mon Sep 17 00:00:00 2001 From: James Perkins Date: Thu, 27 Jul 2017 13:18:51 -0700 Subject: [PATCH 1/2] Remove unused/unneeded SyslogTest. --- pom.xml | 3 - .../logmanager/handlers/SimpleLogServer.java | 178 --------------- .../jboss/logmanager/handlers/SyslogTest.java | 206 ------------------ 3 files changed, 387 deletions(-) delete mode 100644 src/test/java/org/jboss/logmanager/handlers/SimpleLogServer.java delete mode 100644 src/test/java/org/jboss/logmanager/handlers/SyslogTest.java diff --git a/pom.xml b/pom.xml index 50fcfab4..297b2e9c 100644 --- a/pom.xml +++ b/pom.xml @@ -141,9 +141,6 @@ **/*Tests.java - - **/SyslogTest.java - true org.jboss.logmanager.LogManager diff --git a/src/test/java/org/jboss/logmanager/handlers/SimpleLogServer.java b/src/test/java/org/jboss/logmanager/handlers/SimpleLogServer.java deleted file mode 100644 index 4bedb9e6..00000000 --- a/src/test/java/org/jboss/logmanager/handlers/SimpleLogServer.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * - * Copyright 2014 Red Hat, Inc., and individual contributors - * as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jboss.logmanager.handlers; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * @author James R. Perkins - */ -public abstract class SimpleLogServer implements Runnable { - - - protected final AtomicBoolean closed = new AtomicBoolean(false); - protected final BlockingQueue receivedData = new LinkedBlockingQueue(); - - static SimpleLogServer createUdp(final InetAddress address, final int port) throws IOException { - return new SimpleLogServer.Udp(address, port); - } - - static SimpleLogServer createTcp(final InetAddress address, final int port) throws IOException { - final SimpleLogServer server = new SimpleLogServer.Tcp(address, port); - final Thread thread = new Thread(server); - thread.setDaemon(true); - thread.start(); - return server; - } - - static SimpleLogServer createTcp(final OutputStream out, final InetAddress address, final int port) throws IOException { - final SimpleLogServer server = new SimpleLogServer.Tcp(out, address, port); - final Thread thread = new Thread(server); - thread.setDaemon(true); - thread.start(); - return server; - } - - abstract void close(); - - byte[] receiveData() throws InterruptedException { - return receivedData.poll(20, TimeUnit.SECONDS); - } - - byte[] pollData() { - return receivedData.poll(); - } - - static void safeClose(final Closeable closeable) { - if (closeable != null) try { - closeable.close(); - } catch (Exception ignore) { - - } - } - - private static class Udp extends SimpleLogServer { - - private final DatagramSocket socket; - - Udp(InetAddress address, int port) throws IOException { - socket = new DatagramSocket(port); - } - - @Override - void close() { - closed.set(true); - socket.close(); - } - - @Override - public void run() { - - while (!closed.get()) { - try { - DatagramPacket packet = new DatagramPacket(new byte[2048], 2048); - socket.receive(packet); - byte[] bytes = new byte[packet.getLength()]; - System.arraycopy(packet.getData(), 0, bytes, 0, packet.getLength()); - receivedData.add(bytes); - } catch (IOException e) { - if (!closed.get()) { - e.printStackTrace(); - close(); - } - } - } - } - } - - private static class Tcp extends SimpleLogServer { - - private final ServerSocket serverSocket; - private final OutputStream out; - private volatile Socket socket; - - //Socket - Tcp(InetAddress address, int port) throws IOException { - serverSocket = new ServerSocket(port, 50, address); - out = null; - } - - //Socket - Tcp(final OutputStream out, InetAddress address, int port) throws IOException { - serverSocket = new ServerSocket(port, 50, address); - this.out = out; - } - - @Override - void close() { - closed.set(true); - safeClose(serverSocket); - safeClose(socket); - } - - @Override - public void run() { - try { - socket = serverSocket.accept(); - } catch (IOException e) { - e.printStackTrace(); - return; - } - try { - InputStream in = socket.getInputStream(); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - while (!closed.get()) { - byte[] buf = new byte[512]; - int len; - while ((len = in.read(buf)) != -1) { - out.write(buf, 0, len); - if (this.out != null) { - this.out.write(buf, 0, len); - } - } - if (out.toByteArray() != null && out.toByteArray().length > 0) { - receivedData.add(out.toByteArray()); - } - } - if (out.toByteArray() != null && out.toByteArray().length > 0) { - receivedData.add(out.toByteArray()); - } - } catch (IOException e) { - if (!closed.get()) { - e.printStackTrace(); - close(); - } - } - } - } -} diff --git a/src/test/java/org/jboss/logmanager/handlers/SyslogTest.java b/src/test/java/org/jboss/logmanager/handlers/SyslogTest.java deleted file mode 100644 index 9ab699fe..00000000 --- a/src/test/java/org/jboss/logmanager/handlers/SyslogTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * - * Copyright 2014 Red Hat, Inc., and individual contributors - * as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jboss.logmanager.handlers; - -import java.io.IOException; -import java.net.InetAddress; -import java.util.concurrent.TimeUnit; - -import org.jboss.logmanager.ExtHandler; -import org.jboss.logmanager.Logger; -import org.jboss.logmanager.formatters.PatternFormatter; -import org.jboss.logmanager.handlers.AsyncHandler.OverflowAction; -import org.jboss.logmanager.handlers.SyslogHandler.Protocol; -import org.jboss.logmanager.handlers.SyslogHandler.SyslogType; -import org.junit.Before; -import org.junit.Test; - -/** - * These tests should not be run by default and should only be run individually. These are only available for - * convenience. - * - * @author James R. Perkins - */ -public class SyslogTest { - - private String hostname; - private int port; - private boolean useOctetCounting; - private String formatPattern; - private int logCount; - private String encoding; - private String message; - private String delimiter; - - @Before - public void propertyInit() { - hostname = System.getProperty("syslog.hostname", "localhost"); - port = Integer.parseInt(System.getProperty("syslog.port", "514")); - useOctetCounting = Boolean.parseBoolean(System.getProperty("syslog.useOctectCounting", "false")); - formatPattern = System.getProperty("syslog.formatPattern", "%s"); - logCount = Integer.parseInt(System.getProperty("syslog.logCount", "3")); - encoding = System.getProperty("syslog.encoding"); - message = System.getProperty("syslog.message"); - delimiter = System.getProperty("syslog.delimiter", "\n"); - } - - @Test - public void tcpLocal() throws Exception { - port = 10514; - // Setup the handler - final SyslogHandler handler = createHandler(); - handler.setSyslogType(SyslogType.RFC5424); - handler.setProtocol(Protocol.TCP); - handler.setAutoFlush(true); - - // Run the tests - tcpLocal(handler); - } - - @Test - public void tcpLocalAsync() throws Exception { - port = 10514; - // Setup the handler - final SyslogHandler syslogHandler = createHandler(); - syslogHandler.setSyslogType(SyslogType.RFC5424); - syslogHandler.setProtocol(Protocol.TCP); - syslogHandler.setAutoFlush(true); - - final AsyncHandler asyncHandler = new AsyncHandler(); - asyncHandler.setAutoFlush(true); - asyncHandler.addHandler(syslogHandler); - asyncHandler.setOverflowAction(OverflowAction.BLOCK); - // Block until connected - syslogHandler.setBlockOnReconnect(true); - - // Run the tests - tcpLocal(asyncHandler); - } - - @Test - public void tcpSyslog() throws Exception { - // Setup the handler - final SyslogHandler handler = createHandler(); - handler.setSyslogType(SyslogType.RFC5424); - handler.setProtocol(Protocol.TCP); - - doLog(handler); - handler.close(); - } - - @Test - public void sslTcpSyslog() throws Exception { - // Setup the handler - final SyslogHandler handler = createHandler(); - handler.setSyslogType(SyslogType.RFC5424); - handler.setProtocol(Protocol.SSL_TCP); - - doLog(handler); - handler.close(); - } - - @Test - public void udpSyslog() throws Exception { - // Setup the handler - final SyslogHandler handler = createHandler(); - handler.setSyslogType(SyslogType.RFC5424); - handler.setProtocol(Protocol.UDP); - - doLog(handler); - handler.close(); - } - - private void tcpLocal(final ExtHandler handler) throws Exception { - SimpleLogServer server = SimpleLogServer.createTcp(System.out, InetAddress.getLocalHost(), port); - try { - - doLog(handler); - // Sleep just to make sure all is written before we close down the server - TimeUnit.SECONDS.sleep(5L); - - // Close the server and spin up a new one - server.close(); - // Allow a failure - try { - doLog(handler, 3); - } catch (Exception e) { - e.printStackTrace(); - } - // Spin up a new server - server = SimpleLogServer.createTcp(System.out, InetAddress.getLocalHost(), port); - // Sleep again for a moment just to give it chance to reconnect, 6 seconds should do as it should reconnect in 5 - TimeUnit.SECONDS.sleep(6L); - - doLog(handler, 6); - } finally { - handler.flush(); - // Async handlers might require a second to flush - TimeUnit.SECONDS.sleep(1L); - handler.close(); - // Allow everything to finish before we close dow the server - TimeUnit.SECONDS.sleep(2L); - server.close(); - } - } - - private void doLog(final ExtHandler handler) throws Exception { - doLog(handler, 0); - } - - private void doLog(final ExtHandler handler, final int start) throws Exception { - final int end = start + logCount; - final Logger logger = Logger.getLogger(SyslogTest.class.getName()); - logger.addHandler(handler); - for (int i = start; i < end; i++) { - if (message == null) { - logger.warning(String.format("This is a test syslog message. \n Iteration: %d", i)); - } else { - logger.warning(message); - } - } - logger.removeHandler(handler); - } - - private SyslogHandler createHandler() throws IOException { - // Setup the handler - final SyslogHandler handler = new SyslogHandler(hostname, port); - handler.setUseCountingFraming(useOctetCounting); - handler.setEncoding(encoding); - handler.setHostname("localhost"); - handler.setFormatter(new PatternFormatter(formatPattern)); - if (delimiter == null || delimiter.isEmpty()) { - handler.setMessageDelimiter(null); - handler.setUseMessageDelimiter(false); - } else if ("%n".equals(delimiter)) { - handler.setMessageDelimiter("\n"); - handler.setUseMessageDelimiter(true); - } else if ("null".equalsIgnoreCase(delimiter)) { - handler.setMessageDelimiter(null); - handler.setUseMessageDelimiter(true); - } else if ("%t".equalsIgnoreCase(delimiter)) { - handler.setMessageDelimiter("\t"); - handler.setUseMessageDelimiter(true); - } else { - handler.setMessageDelimiter(delimiter); - handler.setUseMessageDelimiter(true); - } - return handler; - } -} From edc79957c5645b663ee99cb42a9088f53655c251 Mon Sep 17 00:00:00 2001 From: James Perkins Date: Thu, 27 Jul 2017 13:37:40 -0700 Subject: [PATCH 2/2] [LOGMGR-156] Add a socket handler. --- pom.xml | 13 + .../logmanager/handlers/SocketHandler.java | 415 ++++++++++++++++++ .../handlers/SslTcpOutputStream.java | 34 ++ .../logmanager/handlers/SimpleServer.java | 183 ++++++++ .../handlers/SocketHandlerTests.java | 129 ++++++ src/test/resources/client-keystore.jks | Bin 0 -> 2135 bytes src/test/resources/client.cer | Bin 0 -> 816 bytes src/test/resources/generate.sh | 38 ++ src/test/resources/server-keystore.jks | Bin 0 -> 2135 bytes src/test/resources/server.cer | Bin 0 -> 818 bytes src/test/resources/test-client-store.jks | Bin 0 -> 1308 bytes src/test/resources/test-server-store.jks | Bin 0 -> 1308 bytes 12 files changed, 812 insertions(+) create mode 100644 src/main/java/org/jboss/logmanager/handlers/SocketHandler.java create mode 100644 src/test/java/org/jboss/logmanager/handlers/SimpleServer.java create mode 100644 src/test/java/org/jboss/logmanager/handlers/SocketHandlerTests.java create mode 100644 src/test/resources/client-keystore.jks create mode 100644 src/test/resources/client.cer create mode 100755 src/test/resources/generate.sh create mode 100644 src/test/resources/server-keystore.jks create mode 100644 src/test/resources/server.cer create mode 100644 src/test/resources/test-client-store.jks create mode 100644 src/test/resources/test-server-store.jks diff --git a/pom.xml b/pom.xml index 297b2e9c..59fc26c2 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,11 @@ 1.3.2.GA + + 127.0.0.1 + 4560 + 14560 + 1.8 1.8 @@ -145,6 +150,14 @@ true org.jboss.logmanager.LogManager ${project.build.directory}${file.separator}logs${file.separator} + + ${project.basedir}/src/test/resources/server-keystore.jks + testpassword + ${project.basedir}/src/test/resources/client-keystore.jks + testpassword + ${org.jboss.test.address} + ${org.jboss.test.port} + ${org.jboss.test.alt.port} diff --git a/src/main/java/org/jboss/logmanager/handlers/SocketHandler.java b/src/main/java/org/jboss/logmanager/handlers/SocketHandler.java new file mode 100644 index 00000000..be4b2367 --- /dev/null +++ b/src/main/java/org/jboss/logmanager/handlers/SocketHandler.java @@ -0,0 +1,415 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2015 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jboss.logmanager.handlers; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; + +import org.jboss.logmanager.ExtHandler; +import org.jboss.logmanager.ExtLogRecord; + +/** + * A handler used to communicate over a socket. + * + * @author James R. Perkins + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class SocketHandler extends ExtHandler { + + /** + * The type of socket + */ + public enum Protocol { + /** + * Transmission Control Protocol + */ + TCP, + /** + * User Datagram Protocol + */ + UDP, + /** + * Transport Layer Security over TCP + */ + SSL_TCP, + } + + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_PORT = 4560; + + private final boolean configureSocketFactory; + + private final Object outputLock = new Object(); + + // All the following fields are guarded by outputLock + private SocketFactory socketFactory; + private InetAddress address; + private int port; + private Protocol protocol; + private boolean blockOnReconnect; + private Writer writer; + private boolean initialize; + + /** + * Creates a socket handler with an address of {@linkplain java.net.InetAddress#getLocalHost() localhost} and port + * of {@linkplain #DEFAULT_PORT 4560}. + * + * @throws UnknownHostException if an error occurs attempting to retrieve the localhost + */ + public SocketHandler() throws UnknownHostException { + this(InetAddress.getLocalHost(), DEFAULT_PORT); + } + + /** + * Creates a socket handler. + * + * @param hostname the hostname to connect to + * @param port the port to connect to + * + * @throws UnknownHostException if an error occurs resolving the address + */ + public SocketHandler(final String hostname, final int port) throws UnknownHostException { + this(InetAddress.getByName(hostname), port); + } + + /** + * Creates a socket handler. + * + * @param address the address to connect to + * @param port the port to connect to + */ + public SocketHandler(final InetAddress address, final int port) { + this(Protocol.TCP, address, port); + } + + /** + * Creates a socket handler. + * + * @param protocol the protocol to connect with + * @param hostname the hostname to connect to + * @param port the port to connect to + * + * @throws UnknownHostException if an error occurs resolving the hostname + */ + public SocketHandler(final Protocol protocol, final String hostname, final int port) throws UnknownHostException { + this(protocol, InetAddress.getByName(hostname), port); + } + + /** + * Creates a socket handler. + * + * @param protocol the protocol to connect with + * @param address the address to connect to + * @param port the port to connect to + */ + public SocketHandler(final Protocol protocol, final InetAddress address, final int port) { + this(null, protocol, address, port); + } + + /** + * Creates a socket handler. + * + * @param socketFactory the socket factory to use for creating {@linkplain Protocol#TCP TCP} or + * {@linkplain Protocol#SSL_TCP SSL TCP} connections, if {@code null} a default factory will + * be used + * @param protocol the protocol to connect with + * @param address the address to connect to + * @param port the port to connect to + */ + public SocketHandler(final SocketFactory socketFactory, final Protocol protocol, final InetAddress address, final int port) { + this.address = address; + this.port = port; + this.protocol = protocol; + initialize = true; + writer = null; + this.socketFactory = socketFactory; + configureSocketFactory = (socketFactory == null); + blockOnReconnect = false; + } + + @Override + protected void doPublish(final ExtLogRecord record) { + final String formatted; + final Formatter formatter = getFormatter(); + try { + formatted = formatter.format(record); + } catch (Exception e) { + reportError("Could not format message", e, ErrorManager.FORMAT_FAILURE); + return; + } + if (formatted.isEmpty()) { + // nothing to write; move along + return; + } + try { + synchronized (outputLock) { + if (initialize) { + initialize(); + initialize = false; + } + if (writer == null) { + return; + } + writer.write(formatted); + super.doPublish(record); + } + } catch (Exception e) { + reportError("Error writing log message", e, ErrorManager.WRITE_FAILURE); + } + } + + @Override + public void flush() { + synchronized (outputLock) { + safeFlush(writer); + } + super.flush(); + } + + @Override + public void close() throws SecurityException { + checkAccess(this); + synchronized (outputLock) { + safeClose(writer); + writer = null; + } + super.close(); + } + + /** + * Returns the address being used. + * + * @return the address + */ + public InetAddress getAddress() { + return address; + } + + /** + * Sets the address to connect to. + * + * @param address the address + */ + public void setAddress(final InetAddress address) { + checkAccess(this); + synchronized (outputLock) { + this.address = address; + initialize = true; + } + } + + /** + * Sets the address to connect to by doing a lookup on the hostname. + * + * @param hostname the host name used to resolve the address + * + * @throws UnknownHostException if an error occurs resolving the address + */ + public void setHostname(final String hostname) throws UnknownHostException { + checkAccess(this); + setAddress(InetAddress.getByName(hostname)); + } + + /** + * Indicates whether or not the output stream is set to block when attempting to reconnect a TCP connection. + * + * @return {@code true} if blocking is enabled, otherwise {@code false} + */ + public boolean isBlockOnReconnect() { + synchronized (outputLock) { + return blockOnReconnect; + } + } + + /** + * Enables or disables blocking when attempting to reconnect the socket when using a {@linkplain Protocol#TCP TCP} + * or {@linkplain Protocol#SSL_TCP SSL TCP} connections. + *

+ * If set to {@code true} the {@code write} methods will block when attempting to reconnect. This is only advisable + * to be set to {@code true} if using an asynchronous handler. + * + * @param blockOnReconnect {@code true} to block when reconnecting or {@code false} to reconnect asynchronously + * discarding any new messages coming in + */ + public void setBlockOnReconnect(final boolean blockOnReconnect) { + checkAccess(this); + synchronized (outputLock) { + this.blockOnReconnect = blockOnReconnect; + } + } + + /** + * Returns the protocol being used. + * + * @return the protocol + */ + public Protocol getProtocol() { + return protocol; + } + + /** + * Sets the protocol to use. + * + * @param protocol the protocol to use + */ + public void setProtocol(final Protocol protocol) { + checkAccess(this); + synchronized (outputLock) { + // If the socket factory wasn't set, we may need to configure the correct factory + if (configureSocketFactory && this.protocol != protocol) { + socketFactory = null; + } + this.protocol = protocol; + initialize = true; + } + } + + /** + * Returns the port being used. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * Sets the port to connect to. + * + * @param port the port + */ + public void setPort(final int port) { + checkAccess(this); + synchronized (outputLock) { + this.port = port; + initialize = true; + } + } + + private void initialize() { + final Writer current = this.writer; + boolean okay = false; + try { + if (current != null) { + writeTail(current); + safeFlush(current); + } + // Close the current writer before we attempt to create a new connection + safeClose(current); + final OutputStream out = createOutputStream(); + if (out == null) { + return; + } + final String encoding = getEncoding(); + final UninterruptibleOutputStream outputStream = new UninterruptibleOutputStream(out); + if (encoding == null) { + writer = new OutputStreamWriter(outputStream); + } else { + writer = new OutputStreamWriter(outputStream, encoding); + } + writeHead(writer); + okay = true; + } catch (UnsupportedEncodingException e) { + reportError("Error opening", e, ErrorManager.OPEN_FAILURE); + } finally { + safeClose(current); + if (!okay) { + safeClose(writer); + } + } + + } + + private OutputStream createOutputStream() { + if (address != null || port >= 0) { + try { + if (protocol == Protocol.SSL_TCP) { + return new SslTcpOutputStream(getSocketFactory(), address, port, blockOnReconnect); + } else if (protocol == Protocol.UDP) { + return new UdpOutputStream(address, port); + } else { + return new TcpOutputStream(getSocketFactory(), address, port, blockOnReconnect); + } + } catch (IOException e) { + reportError("Failed to create socket output stream", e, ErrorManager.OPEN_FAILURE); + } + } + return null; + } + + private void writeHead(final Writer writer) { + try { + final Formatter formatter = getFormatter(); + if (formatter != null) writer.write(formatter.getHead(this)); + } catch (Exception e) { + reportError("Error writing section header", e, ErrorManager.WRITE_FAILURE); + } + } + + private void writeTail(final Writer writer) { + try { + final Formatter formatter = getFormatter(); + if (formatter != null) writer.write(formatter.getTail(this)); + } catch (Exception ex) { + reportError("Error writing section tail", ex, ErrorManager.WRITE_FAILURE); + } + } + + private void safeClose(Closeable c) { + try { + if (c != null) c.close(); + } catch (Exception e) { + reportError("Error closing resource", e, ErrorManager.CLOSE_FAILURE); + } catch (Throwable ignored) { + } + } + + private void safeFlush(Flushable f) { + try { + if (f != null) f.flush(); + } catch (Exception e) { + reportError("Error on flush", e, ErrorManager.FLUSH_FAILURE); + } catch (Throwable ignored) { + } + } + + private SocketFactory getSocketFactory() { + SocketFactory socketFactory = this.socketFactory; + if (socketFactory == null) { + if (protocol == Protocol.TCP) { + this.socketFactory = socketFactory = SocketFactory.getDefault(); + } else if (protocol == Protocol.SSL_TCP) { + this.socketFactory = socketFactory = SSLSocketFactory.getDefault(); + } + } + return socketFactory; + } +} diff --git a/src/main/java/org/jboss/logmanager/handlers/SslTcpOutputStream.java b/src/main/java/org/jboss/logmanager/handlers/SslTcpOutputStream.java index d34d8342..1bd34c3e 100644 --- a/src/main/java/org/jboss/logmanager/handlers/SslTcpOutputStream.java +++ b/src/main/java/org/jboss/logmanager/handlers/SslTcpOutputStream.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.InetAddress; +import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; /** @@ -29,6 +30,7 @@ * * @author James R. Perkins */ +@SuppressWarnings({"unused", "WeakerAccess"}) public class SslTcpOutputStream extends TcpOutputStream implements FlushableCloseable { /** @@ -45,6 +47,21 @@ public SslTcpOutputStream(final InetAddress address, final int port) throws IOEx super(SSLSocketFactory.getDefault(), address, port); } + /** + * Creates a SSL TCP output stream. + *

+ * Uses the {@link javax.net.ssl.SSLSocketFactory#getDefault() default socket factory} to create the socket. + * + * @param socketFactory the factory used to create the socket + * @param address the address to connect to + * @param port the port to connect to + * + * @throws IOException if an I/O error occurs when creating the socket + */ + public SslTcpOutputStream(final SocketFactory socketFactory, final InetAddress address, final int port) throws IOException { + super(socketFactory, address, port); + } + /** * Creates a SSL TCP output stream. *

@@ -60,4 +77,21 @@ public SslTcpOutputStream(final InetAddress address, final int port) throws IOEx public SslTcpOutputStream(final InetAddress address, final int port, final boolean blockOnReconnect) throws IOException { super(SSLSocketFactory.getDefault(), address, port, blockOnReconnect); } + + /** + * Creates a SSL TCP output stream. + *

+ * Uses the {@link javax.net.ssl.SSLSocketFactory#getDefault() default socket factory} to create the socket. + * + * @param socketFactory the factory used to create the socket + * @param address the address to connect to + * @param port the port to connect to + * @param blockOnReconnect {@code true} to block when attempting to reconnect the socket or {@code false} to + * reconnect asynchronously + * + * @throws IOException if an I/O error occurs when creating the socket + */ + public SslTcpOutputStream(final SocketFactory socketFactory, final InetAddress address, final int port, final boolean blockOnReconnect) throws IOException { + super(socketFactory, address, port, blockOnReconnect); + } } diff --git a/src/test/java/org/jboss/logmanager/handlers/SimpleServer.java b/src/test/java/org/jboss/logmanager/handlers/SimpleServer.java new file mode 100644 index 00000000..69bd315e --- /dev/null +++ b/src/test/java/org/jboss/logmanager/handlers/SimpleServer.java @@ -0,0 +1,183 @@ +package org.jboss.logmanager.handlers; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.security.GeneralSecurityException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLServerSocketFactory; + +/** + * @author James R. Perkins + */ +abstract class SimpleServer implements Runnable, AutoCloseable { + + private final BlockingQueue data; + private final ExecutorService service; + + private SimpleServer(final BlockingQueue data) { + this.data = data; + service = Executors.newSingleThreadExecutor(r -> { + final Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); + } + + static SimpleServer createTcpServer(final int port) throws IOException { + final SimpleServer server = new TcpServer(ServerSocketFactory.getDefault(), new LinkedBlockingDeque<>(), port); + server.start(); + return server; + } + + + static SimpleServer createTlsServer(final int port) throws IOException, GeneralSecurityException { + final SimpleServer server = new TcpServer(SSLServerSocketFactory.getDefault(), new LinkedBlockingDeque<>(), port); + server.start(); + return server; + } + + static SimpleServer createUdpServer(final int port) throws IOException { + final SimpleServer server = new UdpServer(new LinkedBlockingDeque<>(), port); + server.start(); + return server; + } + + String poll() throws InterruptedException { + return data.poll(10, TimeUnit.SECONDS); + } + + String peek() { + return data.peek(); + } + + private void start() { + service.submit(this); + } + + @Override + public void close() throws Exception { + service.shutdown(); + service.awaitTermination(30, TimeUnit.SECONDS); + } + + private static class TcpServer extends SimpleServer { + private final BlockingQueue data; + private final AtomicBoolean closed = new AtomicBoolean(true); + private final ServerSocket serverSocket; + private volatile Socket socket; + + private TcpServer(final ServerSocketFactory serverSocketFactory, final BlockingQueue data, final int port) throws IOException { + super(data); + this.serverSocket = serverSocketFactory.createServerSocket(port); + this.data = data; + } + + @Override + public void run() { + closed.set(false); + try { + socket = serverSocket.accept(); + InputStream in = socket.getInputStream(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + while (!closed.get()) { + final byte[] buffer = new byte[512]; + int len; + while ((len = in.read(buffer)) != -1) { + final byte lastByte = buffer[len - 1]; + if (lastByte == '\n') { + out.write(buffer, 0, (len - 1)); + data.put(out.toString()); + out.reset(); + } else { + out.write(buffer, 0, len); + } + } + } + } catch (IOException e) { + if (!closed.get()) { + throw new UncheckedIOException(e); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() throws Exception { + try { + closed.set(true); + try { + socket.close(); + } finally { + serverSocket.close(); + } + } finally { + super.close(); + } + } + } + + private static class UdpServer extends SimpleServer { + private final BlockingQueue data; + private final AtomicBoolean closed = new AtomicBoolean(true); + private final DatagramSocket socket; + + private UdpServer(final BlockingQueue data, final int port) throws SocketException { + super(data); + this.data = data; + socket = new DatagramSocket(port); + } + + @Override + public void run() { + closed.set(false); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + while (!closed.get()) { + try { + final DatagramPacket packet = new DatagramPacket(new byte[2048], 2048); + socket.receive(packet); + final int len = packet.getLength(); + byte[] bytes = new byte[len]; + System.arraycopy(packet.getData(), 0, bytes, 0, len); + final byte lastByte = bytes[len - 1]; + if (lastByte == '\n') { + out.write(bytes, 0, (len - 1)); + data.put(out.toString()); + out.reset(); + } else { + out.write(bytes, 0, len); + } + } catch (IOException e) { + if (!closed.get()) { + throw new UncheckedIOException(e); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void close() throws Exception { + try { + closed.set(true); + socket.close(); + } finally { + super.close(); + } + } + } +} diff --git a/src/test/java/org/jboss/logmanager/handlers/SocketHandlerTests.java b/src/test/java/org/jboss/logmanager/handlers/SocketHandlerTests.java new file mode 100644 index 00000000..606d53b6 --- /dev/null +++ b/src/test/java/org/jboss/logmanager/handlers/SocketHandlerTests.java @@ -0,0 +1,129 @@ +package org.jboss.logmanager.handlers; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.jboss.logmanager.ExtLogRecord; +import org.jboss.logmanager.formatters.PatternFormatter; +import org.jboss.logmanager.handlers.SocketHandler.Protocol; +import org.junit.Assert; +import org.junit.Test; + +/** + * @author James R. Perkins + */ +public class SocketHandlerTests extends AbstractHandlerTest { + + private final InetAddress address; + private final int port; + private final int altPort; + + public SocketHandlerTests() throws UnknownHostException { + address = InetAddress.getByName(System.getProperty("org.jboss.test.address", "127.0.0.1")); + port = Integer.parseInt(System.getProperty("org.jboss.test.port", Integer.toString(SocketHandler.DEFAULT_PORT))); + altPort = Integer.parseInt(System.getProperty("org.jboss.test.alt.port", Integer.toString(SocketHandler.DEFAULT_PORT + 10000))); + } + + @Test + public void testTcpConnection() throws Exception { + try ( + SimpleServer server = SimpleServer.createTcpServer(port); + SocketHandler handler = createHandler(Protocol.TCP) + ) { + final ExtLogRecord record = createLogRecord("Test TCP handler"); + handler.doPublish(record); + final String msg = server.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test TCP handler", msg); + } + } + + @Test + public void testTlsConnection() throws Exception { + try ( + SimpleServer server = SimpleServer.createTlsServer(port); + SocketHandler handler = createHandler(Protocol.SSL_TCP) + ) { + final ExtLogRecord record = createLogRecord("Test TLS handler"); + handler.doPublish(record); + final String msg = server.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test TLS handler", msg); + } + } + + @Test + public void testUdpConnection() throws Exception { + try ( + SimpleServer server = SimpleServer.createUdpServer(port); + SocketHandler handler = createHandler(Protocol.UDP) + ) { + final ExtLogRecord record = createLogRecord("Test UDP handler"); + handler.doPublish(record); + final String msg = server.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test UDP handler", msg); + } + } + + @Test + public void testTcpPortChange() throws Exception { + try ( + SimpleServer server1 = SimpleServer.createTcpServer(port); + SimpleServer server2 = SimpleServer.createTcpServer(altPort); + SocketHandler handler = createHandler(Protocol.TCP) + ) { + ExtLogRecord record = createLogRecord("Test TCP handler " + port); + handler.doPublish(record); + String msg = server1.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test TCP handler " + port, msg); + + // Change the port on the handler which should close the first connection and open a new one + handler.setPort(altPort); + record = createLogRecord("Test TCP handler " + altPort); + handler.doPublish(record); + msg = server2.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test TCP handler " + altPort, msg); + + // There should be nothing on server1, we won't know if the real connection is closed but we shouldn't + // have any data remaining on the first server + Assert.assertNull("Expected no data on server1", server1.peek()); + } + } + + @Test + public void testProtocolChange() throws Exception { + try (SocketHandler handler = createHandler(Protocol.TCP)) { + try (SimpleServer server = SimpleServer.createTcpServer(port)) { + final ExtLogRecord record = createLogRecord("Test TCP handler"); + handler.doPublish(record); + final String msg = server.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test TCP handler", msg); + } + + // Change the protocol on the handler which should close the first connection and open a new one + handler.setProtocol(Protocol.SSL_TCP); + + try (SimpleServer server = SimpleServer.createTlsServer(port)) { + final ExtLogRecord record = createLogRecord("Test TLS handler"); + handler.doPublish(record); + final String msg = server.poll(); + Assert.assertNotNull(msg); + Assert.assertEquals("Test TLS handler", msg); + } + } + } + + private SocketHandler createHandler(final Protocol protocol) throws UnsupportedEncodingException { + final SocketHandler handler = new SocketHandler(protocol, address, port); + handler.setAutoFlush(true); + handler.setEncoding("utf-8"); + handler.setFormatter(new PatternFormatter("%s\n")); + + return handler; + } +} diff --git a/src/test/resources/client-keystore.jks b/src/test/resources/client-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..608c260bd417210cc35eaed8434ba380fb9622bd GIT binary patch literal 2135 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/server.cer b/src/test/resources/server.cer new file mode 100644 index 0000000000000000000000000000000000000000..e5c2b91ba92aac1c5f973597045c1c4f77601c6e GIT binary patch literal 818 zcmXqLV%9TgVtT!RnTe5!iKU#$_niSZ8@pDU$2kiYW>yA+3`1@MPB!LH7B*p~&|nyc zLzv0W*-*fM4iWmri)Nl)P2d5U5r4}iK=4F-` z3KPKBaHDLgkY><*uQ_^eo;?31xIM_`y$i!(Is1lffohqqu?|XszawpX zBYy2a`gNDVx6`vjbFMd9H^`dge@aW(o;@>Q>5}>LU$9gQC>`?Oz4H3M?Z>2lScgA9 zwqltmKg;jtZss$WDKSgVz39PsoJo{ndO|?Is51AN zO_O#uEqdL;{UJi;E68czA1-LUHrw{z={>7s%yxwDtemob_b;ngO+UkZxNX^+(gP#I z`*vN4^nX^!F3{Mp#Z#$nDF-VoJKt-mOZo)tLhG^>D73!c%7W~=4GW5^A^{0 z|57wf`uIDkic!V-%#x4$Rs>GyT+w|0Vbs(4Y?Hdv;u@+$RZ|#!tpA<&)M9RIWdOP3 zXXclH23>}%is)g&ocVTpn8#6RUNjjy)MS$B5!$=w$j z)*sriFQL+cb!$*V^xo>c<5PD0$SL%)z9!W2t53}2`w!{4n+u<2r1YtN-JD_LJZA~p zvHA?zSEoDHGc8Q(T5WKRdE#BupaR#b(kJ0Nrr+mYtY{!_APY=evV1IJEFw=g6+f1F z$;2CbUUsQ6Tcw$aJt7m)5ZXkzR(;A7*`W@BVw)M653WMpMv zX<{s|lRs$8cx2tntm#5eb}B_re_E^``Zq_1|5wMf_7y@q3f*jbHy_PWZCNkGcr;pW zr~i~=OB|carLFpfZufTtikwSl-OBT9)lD!PR0NnLxs z<#jw+mhyD^U)TNjH~->Xbm6^b?JC0;^1F9_i}e?4DxFfj>rG_$!xQt|LQ2+X zrFrI&_`i;OxpQ1tg}pPH1#aH0Idg4~z@vw4D|~z8<*u*?9eCdRcze?N`FfQhvhhzV zlP>LF?ab>fEXMU~XCaHEiuX*5(q?XpQ{JJMGXFFc?Pxjca!n&c=bGDJQ@+i+oB8f` zFgI=fH|bVM&Dk?jW}KXF{`t(za4)PE$@E!yg%&Z^Zi zXY@b6W87I0cUj@ZuBx?hNzTRdelGgs@c*FI8pjoRc@H|l(Zm{|XKG-{z`*Qg(8TNn z#0(3VnHZUvSPbGkI1ISi*tOa`&RMW9voaVI8*&?PvN4CUun9AT2E#ZU!c2b7h62EN z1XH)fJC^2d4p0@6g(13bQC=ElJyKl41_@9+``em&~94f~8tO>5%{KmDm4m zKPLUdI{f*u70X2VS$;QvyL?pcFq3JBaFp0Yqi?03*J&qS*Lwd+iCJ>)MGwZ~Ori|a z69W20mATJsnzXZN(d!oO4-qn7K~DSra6#*}*|zsi?^zvVwj+FJ<&^cie_6e1`Wf!S zZOh)29vB(kx9duz|Fc4NfyRa{o??Z{FMKq_gtwpPH0n9A?4gZVRd+~Duf~JL>*TaI zFDspxx454Bm!fIX$KOd+j4IY=mVDf|B5*?Iist(dqn^%Zo79~a*H9g*n!@N~{qMY| z7IR|@15;x|-EHq}zOhpuU3?(E%~vPjNYMH1-yi;wcr&@{wdk8|N6XYi-ei40?VuaT zzsvgg_1nqe_CfZ)`(G&4Y4WiCQRSRFMfp&i+?v~aJoFMKas^3o2(g<9O#90=HToOt zbXLan?$wpMAKtk0f@Ol~p6O2v7tjnt@UjF%g>!A&k%YnXd(kjqG#4;FwkKVDY4wY!RpY#BX%pjLMNZo*=w1} y@qtv*kquP4{2o{iIe*_?&jpL&{fE)rz% zP!la)ld|j)x9XOc2g)BRpK5)2J*V5aXV;7;5l+X?^rgQ|4_ zJ(&kyLh!cN0)frHAI*`uHs{*n^|QnBe>RFQ5VX4ZFS$LPK&#zHQS^F9)7U&TyhBeH`Zg4GiDt+wjjs)`d_Y{@3=b7 zuF$+58h`)i<-?J`$~BG^F!scMF?qPRHxk8>6*%&ZIs#fIDloNUaYENsF|p}{Z?hcJ_$v!MVm zUV$>~!t6nbIjNcH8HQp8A|MeiVcwwB6a|mO5*-E4yktE?5d$HRIJYpjYhHS0UTSJl zW?s6XxPd50lv$XsB(=CiFDog(xL7a0DBVC#oY&CYz}(2p$k^Dzz%UBLH8e0XfpQ1N zQB927foWL>7C4MRjyzLiBg5bN;6~X}AML6=3H;IZjd#}|CE-nJ$q)r(k1ifzhJ2rP&(wld*$_i z+mA{AunvEIY{fEBewN?O-!31OJIrJnA{-?)(db+0=XKhN*R|e%Qeu{zd(ngOIFl&D z^n`$ZQDyEkn?q61~ntq1+ zaNDvsr3Xfa_wBk8>Hn;dU7)dHi>Fwj@(UjgG2!j!IgNTwEPH4pR@EI+)2s1d@j5x} z&C5zB<}I%0{-tP|^znC66{CvvnI#|htq7dZxuW_0!>Fh8*(P$>DO?a3$CJ zf{MmIx$nMAwkKasORTReuu>16X%@k@W0UA(qc;aMn(TWT-zOh&*y2_z$Z4*4@N|s* z$x|IGpZz#p`8g|kZBF>Fcb>