Last active
August 29, 2015 14:09
-
-
Save stuart-warren/7ea3702f8d312e2ee464 to your computer and use it in GitHub Desktop.
Access recent (and tail new) logs from your Log4j Java app over a TLS connection using ncat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Licensed to the Apache Software Foundation (ASF) under one or more | |
* contributor license agreements. See the NOTICE file distributed with | |
* this work for additional information regarding copyright ownership. | |
* The ASF licenses this file to You 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 com.stuartwarren.logging; | |
import java.io.BufferedReader; | |
import java.io.IOException; | |
import java.io.InputStreamReader; | |
import java.io.InterruptedIOException; | |
import java.io.PrintWriter; | |
import java.net.InetSocketAddress; | |
import java.net.ServerSocket; | |
import java.net.Socket; | |
import java.security.cert.Certificate; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.Iterator; | |
import java.util.List; | |
import javax.net.ssl.SSLSocket; | |
import javax.net.ssl.SSLSocketFactory; | |
import org.apache.commons.collections.Buffer; | |
import org.apache.commons.collections.BufferUtils; | |
import org.apache.commons.collections.buffer.CircularFifoBuffer; | |
import org.apache.log4j.AppenderSkeleton; | |
import org.apache.log4j.helpers.LogLog; | |
import org.apache.log4j.spi.LoggingEvent; | |
/** | |
<p>The TelnetAppender is a log4j appender that specializes in | |
writing to a read-only socket. The output is provided in a | |
telnet-friendly way so that a log can be monitored over TCP/IP. | |
Clients using telnet connect to the socket and receive log data. | |
This is handy for remote monitoring, especially when monitoring a | |
servlet. | |
<p>Here is a list of the available configuration options: | |
<table border=1> | |
<tr> | |
<th>Name</th> | |
<th>Requirement</th> | |
<th>Description</th> | |
<th>Sample Value</th> | |
</tr> | |
<tr> | |
<td>Port</td> | |
<td>optional</td> | |
<td>This parameter determines the port to use for announcing log events. The default port is 23 (telnet).</td> | |
<td>5875</td> | |
</tr> | |
<tr> | |
<td>BufferLength</td> | |
<td>optional</td> | |
<td>This parameter determines the size of buffered logs to send to new clients. The default length is 10.</td> | |
<td>100</td> | |
</tr> | |
<tr> | |
<td>MaxConnections</td> | |
<td>optional</td> | |
<td>This parameter determines the maximum number of connected clients. The default is 20.</td> | |
<td>100</td> | |
</tr> | |
<tr> | |
<td>TlsRequired</td> | |
<td>optional</td> | |
<td>This parameter determines whether to require clients to TLS handshake on connection. The default is True.</td> | |
<td>False</td> | |
</tr> | |
<tr> | |
<td>Password</td> | |
<td>optional</td> | |
<td>This parameter specifies a password that must be sent to retrieve logs. The default is blank.</td> | |
<td>MySup3rP@5sw0rd</td> | |
</tr> | |
</table> | |
<p>using ncat from nmap: <br>ncat --ssl localhost 23 | |
@author <a HREF="mailto:jay@v-wave.com">Jay Funnell</a> | |
@author stuart-warren | |
*/ | |
public class BufferedTelnetAppender extends AppenderSkeleton { | |
private SocketHandler sh; | |
private int port = 23; | |
private int bufferLength = 10; | |
private int maxConnections = 20; | |
private boolean tlsRequired = true; | |
private boolean tlsClientAuthRequired = false; | |
private String password = ""; | |
/** | |
This appender requires a layout to format the text to the | |
attached client(s). */ | |
public boolean requiresLayout() { | |
return true; | |
} | |
/** all of the options have been set, create the socket handler and | |
wait for connections. */ | |
public void activateOptions() { | |
try { | |
sh = new SocketHandler(port); | |
sh.start(); | |
} | |
catch(InterruptedIOException e) { | |
Thread.currentThread().interrupt(); | |
e.printStackTrace(); | |
} catch(IOException e) { | |
e.printStackTrace(); | |
} catch(RuntimeException e) { | |
e.printStackTrace(); | |
} | |
super.activateOptions(); | |
} | |
public | |
int getPort() { | |
return port; | |
} | |
public | |
void setPort(int port) { | |
this.port = port; | |
} | |
public int getBufferLength() { | |
return bufferLength; | |
} | |
public void setBufferLength(int bufferLength) { | |
this.bufferLength = bufferLength; | |
} | |
public int getMaxConnections() { | |
return maxConnections; | |
} | |
public void setMaxConnections(int maxConnections) { | |
this.maxConnections = maxConnections; | |
} | |
public boolean isTlsRequired() { | |
return tlsRequired; | |
} | |
public boolean getTlsRequired() { | |
return isTlsRequired(); | |
} | |
public void setTlsRequired(boolean tlsRequired) { | |
this.tlsRequired = tlsRequired; | |
} | |
public boolean isTlsClientAuthRequired() { | |
return tlsClientAuthRequired; | |
} | |
public void setTlsClientAuthRequired(boolean tlsClientAuthRequired) { | |
this.tlsClientAuthRequired = tlsClientAuthRequired; | |
} | |
public String getPassword() { | |
return password; | |
} | |
public void setPassword(String password) { | |
this.password = password; | |
} | |
/** shuts down the appender. */ | |
public void close() { | |
if (sh != null) { | |
sh.close(); | |
try { | |
sh.join(); | |
} catch(InterruptedException ex) { | |
Thread.currentThread().interrupt(); | |
} | |
} | |
} | |
/** Handles a log event. For this appender, that means writing the | |
message to each connected client. */ | |
protected void append(LoggingEvent event) { | |
if(sh != null) { | |
sh.send(layout.format(event)); | |
if(layout.ignoresThrowable()) { | |
String[] s = event.getThrowableStrRep(); | |
if (s != null) { | |
StringBuffer buf = new StringBuffer(); | |
for(int i = 0; i < s.length; i++) { | |
buf.append(s[i]); | |
buf.append("\r\n"); | |
} | |
sh.send(buf.toString()); | |
} | |
} | |
} | |
} | |
//---------------------------------------------------------- SocketHandler: | |
/** The SocketHandler class is used to accept connections from | |
clients. It is threaded so that clients can connect/disconnect | |
asynchronously. */ | |
protected class SocketHandler extends Thread { | |
private List<PrintWriter> writers = Collections.synchronizedList(new ArrayList<PrintWriter>()); | |
private List<Socket> connections = Collections.synchronizedList(new ArrayList<Socket>()); | |
private Buffer fifo; | |
private ServerSocket serverSocket; | |
public void finalize() { | |
close(); | |
} | |
public void close() { | |
synchronized(this) { | |
Iterator<Socket> socketIterator = connections.iterator(); | |
while (socketIterator.hasNext()){ | |
try { | |
socketIterator.next().close(); | |
} catch(InterruptedIOException ex) { | |
Thread.currentThread().interrupt(); | |
} catch(IOException ex) { | |
} catch(RuntimeException ex) { | |
} | |
} | |
} | |
try { | |
serverSocket.close(); | |
} catch(InterruptedIOException ex) { | |
Thread.currentThread().interrupt(); | |
} catch(IOException ex) { | |
} catch(RuntimeException ex) { | |
} | |
} | |
/** sends a message to each of the clients in telnet-friendly output. */ | |
public synchronized void send(final String message) { | |
@SuppressWarnings({ "unchecked", "unused" }) | |
boolean add = fifo.add(message); | |
Iterator<Socket> ce = connections.iterator(); | |
for(Iterator<PrintWriter> e = writers.iterator();e.hasNext();) { | |
ce.next(); | |
PrintWriter writer = e.next(); | |
writer.print(message); | |
if(writer.checkError()) { | |
ce.remove(); | |
e.remove(); | |
} | |
} | |
} | |
/** | |
Continually accepts client connections. Client connections | |
are refused when maxConnections is reached. | |
*/ | |
public void run() { | |
while(!serverSocket.isClosed()) { | |
try { | |
Socket newClient = serverSocket.accept(); | |
if (isTlsRequired()) { | |
// Basic insecure TLS | |
SSLSocketFactory sf = ((SSLSocketFactory) SSLSocketFactory.getDefault()); | |
InetSocketAddress remoteAddress = (InetSocketAddress) newClient.getRemoteSocketAddress(); | |
SSLSocket s = (SSLSocket) (sf.createSocket(newClient, remoteAddress.getHostName(), newClient.getPort(), true)); | |
s.setUseClientMode(false); | |
s.setEnabledProtocols(s.getSupportedProtocols()); | |
s.setEnabledCipherSuites(s.getSupportedCipherSuites()); | |
if (isTlsClientAuthRequired()) { | |
//// Client must authenticate | |
s.setWantClientAuth(true); | |
Certificate[] peerCertificates = s.getSession().getPeerCertificates(); | |
System.out.println(peerCertificates[0].getPublicKey().getEncoded()); | |
} | |
s.startHandshake(); | |
newClient = s; | |
} | |
PrintWriter pw = new PrintWriter(newClient.getOutputStream()); | |
if(connections.size() < maxConnections) { | |
synchronized(this) { | |
connections.add(newClient); | |
writers.add(pw); | |
pw.print("TelnetAppender v1.0 (" + connections.size() | |
+ "/" + maxConnections + " active connections)\r\n\r\n"); | |
if (!password.isEmpty()) { | |
BufferedReader br = new BufferedReader(new InputStreamReader(newClient.getInputStream())); | |
String line = ""; | |
do { | |
pw.print("Enter password:\r\n"); | |
pw.flush(); | |
line = br.readLine(); | |
} while(!line.equals(password)); | |
} | |
for(@SuppressWarnings("unchecked") | |
Iterator<String> fi = fifo.iterator(); fi.hasNext();) { | |
pw.print(fi.next()); | |
} | |
pw.flush(); | |
} | |
} else { | |
pw.print("Too many connections.\r\n"); | |
pw.flush(); | |
newClient.close(); | |
} | |
} catch(Exception e) { | |
if (e instanceof InterruptedIOException || e instanceof InterruptedException) { | |
Thread.currentThread().interrupt(); | |
} | |
if (!serverSocket.isClosed()) { | |
LogLog.error("Encountered error while in SocketHandler loop.", e); | |
} | |
break; | |
} | |
} | |
try { | |
serverSocket.close(); | |
} catch(InterruptedIOException ex) { | |
Thread.currentThread().interrupt(); | |
} catch(IOException ex) { | |
} | |
} | |
public SocketHandler(int port) throws IOException { | |
fifo = BufferUtils.synchronizedBuffer(new CircularFifoBuffer(bufferLength)); | |
serverSocket = new ServerSocket(port); | |
setName("TelnetAppender-" + getName() + "-" + port); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment