diff --git a/SOURCES/0001-Backport-fix-for-CVE-2017-5645.patch b/SOURCES/0001-Backport-fix-for-CVE-2017-5645.patch index 704ecfc..acb6e3d 100644 --- a/SOURCES/0001-Backport-fix-for-CVE-2017-5645.patch +++ b/SOURCES/0001-Backport-fix-for-CVE-2017-5645.patch @@ -1,20 +1,20 @@ -From ea4609eca531916ac347686c048bebdb7b4b6e0d Mon Sep 17 00:00:00 2001 +From 39f12f04d0b138377194b7aa43aa16a2ff401f98 Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Fri, 2 Jun 2017 14:37:35 +0200 Subject: [PATCH] Backport fix for CVE-2017-5645 --- - .../apache/log4j/FilteredObjectInputStream.java | 65 ++++++++++++++++++++++ - src/main/java/org/apache/log4j/net/SocketNode.java | 17 +++++- - 2 files changed, 80 insertions(+), 2 deletions(-) + .../log4j/FilteredObjectInputStream.java | 76 +++++++++++++++++++ + .../java/org/apache/log4j/net/SocketNode.java | 9 ++- + 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/apache/log4j/FilteredObjectInputStream.java diff --git a/src/main/java/org/apache/log4j/FilteredObjectInputStream.java b/src/main/java/org/apache/log4j/FilteredObjectInputStream.java new file mode 100644 -index 0000000..b9ef20c +index 00000000..ea597308 --- /dev/null +++ b/src/main/java/org/apache/log4j/FilteredObjectInputStream.java -@@ -0,0 +1,65 @@ +@@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with @@ -33,15 +33,16 @@ index 0000000..b9ef20c + */ +package org.apache.log4j; + -+import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; ++import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; -+import java.util.List; ++import java.util.HashSet; ++import java.util.Set; + +/** + * Extended ObjectInputStream that only allows certain classes to be deserialized. @@ -50,13 +51,15 @@ index 0000000..b9ef20c + */ +public class FilteredObjectInputStream extends ObjectInputStream { + -+ private static final List REQUIRED_JAVA_CLASSES = Arrays.asList(new String[] { ++ private static final Set REQUIRED_JAVA_CLASSES = new HashSet(Arrays.asList(new String[] { + // Types of non-trainsient fields of LoggingEvent + "java.lang.String", + "java.util.Hashtable", + // ThrowableInformation + "[Ljava.lang.String;" -+ }); ++ })); ++ ++ public static final Collection SYSTEM_ALLOWED_CLASSES = getAllowedClasses(); + + private final Collection allowedClasses; + @@ -73,6 +76,14 @@ index 0000000..b9ef20c + return super.resolveClass(desc); + } + ++ private static Collection getAllowedClasses() { ++ Collection allowedClasses = new HashSet(); ++ String property = System.getProperty("org.apache.log4j.net.allowedClasses"); ++ if (property != null) ++ allowedClasses.addAll(Arrays.asList(property.split(","))); ++ return allowedClasses; ++ } ++ + private static boolean isAllowedByDefault(final String name) { + return name.startsWith("org.apache.log4j.") || + name.startsWith("[Lorg.apache.log4j.") || @@ -81,7 +92,7 @@ index 0000000..b9ef20c + +} diff --git a/src/main/java/org/apache/log4j/net/SocketNode.java b/src/main/java/org/apache/log4j/net/SocketNode.java -index e977f13..f95bb10 100644 +index e977f133..692da289 100644 --- a/src/main/java/org/apache/log4j/net/SocketNode.java +++ b/src/main/java/org/apache/log4j/net/SocketNode.java @@ -22,6 +22,10 @@ import java.io.IOException; @@ -103,25 +114,10 @@ index e977f13..f95bb10 100644 - new BufferedInputStream(socket.getInputStream())); + ois = new FilteredObjectInputStream( + new BufferedInputStream(socket.getInputStream()), -+ getAllowedClasses()); ++ FilteredObjectInputStream.SYSTEM_ALLOWED_CLASSES); } catch(InterruptedIOException e) { Thread.currentThread().interrupt(); logger.error("Could not open ObjectInputStream to "+socket, e); -@@ -65,6 +70,14 @@ public class SocketNode implements Runnable { - } - } - -+ private Collection getAllowedClasses() { -+ Collection allowedClasses = new ArrayList(); -+ String property = System.getProperty("org.apache.log4j.net.allowedClasses"); -+ if (property != null) -+ allowedClasses.addAll(Arrays.asList(property.split(","))); -+ return allowedClasses; -+ } -+ - //public - //void finalize() { - //System.err.println("-------------------------Finalize called"); -- -2.9.4 +2.33.1 diff --git a/SOURCES/0001-Fix-CVE-2022-23302-JMSSink.patch b/SOURCES/0001-Fix-CVE-2022-23302-JMSSink.patch new file mode 100644 index 0000000..2ca8290 --- /dev/null +++ b/SOURCES/0001-Fix-CVE-2022-23302-JMSSink.patch @@ -0,0 +1,172 @@ +From 70345b5e5a6ad37399911194f0b746094061b399 Mon Sep 17 00:00:00 2001 +From: Mikolaj Izdebski +Date: Wed, 2 Feb 2022 20:07:09 +0100 +Subject: [PATCH] Fix CVE-2022-23302 JMSSink + +--- + .../java/org/apache/log4j/net/JMSSink.java | 153 ------------------ + 1 file changed, 153 deletions(-) + delete mode 100644 src/main/java/org/apache/log4j/net/JMSSink.java + +diff --git a/src/main/java/org/apache/log4j/net/JMSSink.java b/src/main/java/org/apache/log4j/net/JMSSink.java +deleted file mode 100644 +index 6a02831e..00000000 +--- a/src/main/java/org/apache/log4j/net/JMSSink.java ++++ /dev/null +@@ -1,153 +0,0 @@ +-/* +- * 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 org.apache.log4j.net; +- +-import org.apache.log4j.Logger; +-import org.apache.log4j.PropertyConfigurator; +-import org.apache.log4j.spi.LoggingEvent; +-import org.apache.log4j.xml.DOMConfigurator; +- +-import javax.jms.JMSException; +-import javax.jms.ObjectMessage; +-import javax.jms.Session; +-import javax.jms.Topic; +-import javax.jms.TopicConnection; +-import javax.jms.TopicConnectionFactory; +-import javax.jms.TopicSession; +-import javax.jms.TopicSubscriber; +-import javax.naming.Context; +-import javax.naming.InitialContext; +-import javax.naming.NameNotFoundException; +-import javax.naming.NamingException; +-import java.io.BufferedReader; +-import java.io.InputStreamReader; +- +-/** +- * A simple application that consumes logging events sent by a {@link +- * JMSAppender}. +- * +- * +- * @author Ceki Gülcü +- * */ +-public class JMSSink implements javax.jms.MessageListener { +- +- static Logger logger = Logger.getLogger(JMSSink.class); +- +- static public void main(String[] args) throws Exception { +- if(args.length != 5) { +- usage("Wrong number of arguments."); +- } +- +- String tcfBindingName = args[0]; +- String topicBindingName = args[1]; +- String username = args[2]; +- String password = args[3]; +- +- +- String configFile = args[4]; +- +- if(configFile.endsWith(".xml")) { +- DOMConfigurator.configure(configFile); +- } else { +- PropertyConfigurator.configure(configFile); +- } +- +- new JMSSink(tcfBindingName, topicBindingName, username, password); +- +- BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); +- // Loop until the word "exit" is typed +- System.out.println("Type \"exit\" to quit JMSSink."); +- while(true){ +- String s = stdin.readLine( ); +- if (s.equalsIgnoreCase("exit")) { +- System.out.println("Exiting. Kill the application if it does not exit " +- + "due to daemon threads."); +- return; +- } +- } +- } +- +- public JMSSink( String tcfBindingName, String topicBindingName, String username, +- String password) { +- +- try { +- Context ctx = new InitialContext(); +- TopicConnectionFactory topicConnectionFactory; +- topicConnectionFactory = (TopicConnectionFactory) lookup(ctx, +- tcfBindingName); +- +- TopicConnection topicConnection = +- topicConnectionFactory.createTopicConnection(username, +- password); +- topicConnection.start(); +- +- TopicSession topicSession = topicConnection.createTopicSession(false, +- Session.AUTO_ACKNOWLEDGE); +- +- Topic topic = (Topic)ctx.lookup(topicBindingName); +- +- TopicSubscriber topicSubscriber = topicSession.createSubscriber(topic); +- +- topicSubscriber.setMessageListener(this); +- +- } catch(JMSException e) { +- logger.error("Could not read JMS message.", e); +- } catch(NamingException e) { +- logger.error("Could not read JMS message.", e); +- } catch(RuntimeException e) { +- logger.error("Could not read JMS message.", e); +- } +- } +- +- public void onMessage(javax.jms.Message message) { +- LoggingEvent event; +- Logger remoteLogger; +- +- try { +- if(message instanceof ObjectMessage) { +- ObjectMessage objectMessage = (ObjectMessage) message; +- event = (LoggingEvent) objectMessage.getObject(); +- remoteLogger = Logger.getLogger(event.getLoggerName()); +- remoteLogger.callAppenders(event); +- } else { +- logger.warn("Received message is of type "+message.getJMSType() +- +", was expecting ObjectMessage."); +- } +- } catch(JMSException jmse) { +- logger.error("Exception thrown while processing incoming message.", +- jmse); +- } +- } +- +- +- protected static Object lookup(Context ctx, String name) throws NamingException { +- try { +- return ctx.lookup(name); +- } catch(NameNotFoundException e) { +- logger.error("Could not find name ["+name+"]."); +- throw e; +- } +- } +- +- static void usage(String msg) { +- System.err.println(msg); +- System.err.println("Usage: java " + JMSSink.class.getName() +- + " TopicConnectionFactoryBindingName TopicBindingName username password configFile"); +- System.exit(1); +- } +-} +-- +2.33.1 + diff --git a/SOURCES/0001-Fix-CVE-2022-23305-JDBCAppender.patch b/SOURCES/0001-Fix-CVE-2022-23305-JDBCAppender.patch new file mode 100644 index 0000000..67b8c09 --- /dev/null +++ b/SOURCES/0001-Fix-CVE-2022-23305-JDBCAppender.patch @@ -0,0 +1,1895 @@ +From 6370372f5a04cdec6598fa8199b762ce33fa4d40 Mon Sep 17 00:00:00 2001 +From: Mikolaj Izdebski +Date: Wed, 2 Feb 2022 19:37:17 +0100 +Subject: [PATCH] Fix CVE-2022-23305 JDBCAppender + +--- + .../apache/log4j/helpers/PatternParser.java | 944 +++++++++--------- + .../org/apache/log4j/jdbc/JDBCAppender.java | 715 ++++++------- + .../apache/log4j/jdbc/JdbcPatternParser.java | 115 +++ + 3 files changed, 942 insertions(+), 832 deletions(-) + create mode 100644 src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java + +diff --git a/src/main/java/org/apache/log4j/helpers/PatternParser.java b/src/main/java/org/apache/log4j/helpers/PatternParser.java +index 0d3ead67..4b169a2d 100644 +--- a/src/main/java/org/apache/log4j/helpers/PatternParser.java ++++ b/src/main/java/org/apache/log4j/helpers/PatternParser.java +@@ -30,541 +30,503 @@ import java.util.Arrays; + // Reinhard Deschler + + /** +- Most of the work of the {@link org.apache.log4j.PatternLayout} class +- is delegated to the PatternParser class. +- +-

It is this class that parses conversion patterns and creates +- a chained list of {@link OptionConverter OptionConverters}. +- +- @author James P. Cakalic +- @author Ceki Gülcü +- @author Anders Kristensen +- +- @since 0.8.2 +-*/ ++ * Most of the work of the {@link org.apache.log4j.PatternLayout} class is ++ * delegated to the PatternParser class. ++ * ++ *

++ * It is this class that parses conversion patterns and creates a chained list ++ * of {@link OptionConverter OptionConverters}. ++ * ++ * @author James P. Cakalic ++ * @author Ceki Gülcü ++ * @author Anders Kristensen ++ * ++ * @since 0.8.2 ++ */ + public class PatternParser { + +- private static final char ESCAPE_CHAR = '%'; +- +- private static final int LITERAL_STATE = 0; +- private static final int CONVERTER_STATE = 1; +- private static final int DOT_STATE = 3; +- private static final int MIN_STATE = 4; +- private static final int MAX_STATE = 5; +- +- static final int FULL_LOCATION_CONVERTER = 1000; +- static final int METHOD_LOCATION_CONVERTER = 1001; +- static final int CLASS_LOCATION_CONVERTER = 1002; +- static final int LINE_LOCATION_CONVERTER = 1003; +- static final int FILE_LOCATION_CONVERTER = 1004; +- +- static final int RELATIVE_TIME_CONVERTER = 2000; +- static final int THREAD_CONVERTER = 2001; +- static final int LEVEL_CONVERTER = 2002; +- static final int NDC_CONVERTER = 2003; +- static final int MESSAGE_CONVERTER = 2004; +- +- int state; +- protected StringBuffer currentLiteral = new StringBuffer(32); +- protected int patternLength; +- protected int i; +- PatternConverter head; +- PatternConverter tail; +- protected FormattingInfo formattingInfo = new FormattingInfo(); +- protected String pattern; +- +- public +- PatternParser(String pattern) { +- this.pattern = pattern; +- patternLength = pattern.length(); +- state = LITERAL_STATE; +- } +- +- private +- void addToList(PatternConverter pc) { +- if(head == null) { +- head = tail = pc; +- } else { +- tail.next = pc; +- tail = pc; ++ private static final char LEFT_BRACKET = '{'; ++ private static final char RIGHT_BRACKET = '}'; ++ private static final char N_CHAR = 'n'; ++ private static final char DOT_CHAR = '.'; ++ private static final char DASH_CHAR = '-'; ++ private static final char ESCAPE_CHAR = '%'; ++ ++ private static final int LITERAL_STATE = 0; ++ private static final int CONVERTER_STATE = 1; ++ private static final int DOT_STATE = 3; ++ private static final int MIN_STATE = 4; ++ private static final int MAX_STATE = 5; ++ ++ static final int FULL_LOCATION_CONVERTER = 1000; ++ static final int METHOD_LOCATION_CONVERTER = 1001; ++ static final int CLASS_LOCATION_CONVERTER = 1002; ++ static final int LINE_LOCATION_CONVERTER = 1003; ++ static final int FILE_LOCATION_CONVERTER = 1004; ++ ++ static final int RELATIVE_TIME_CONVERTER = 2000; ++ static final int THREAD_CONVERTER = 2001; ++ static final int LEVEL_CONVERTER = 2002; ++ static final int NDC_CONVERTER = 2003; ++ static final int MESSAGE_CONVERTER = 2004; ++ ++ int state; ++ protected StringBuffer currentLiteral = new StringBuffer(32); ++ protected int patternLength; ++ protected int i; ++ PatternConverter head; ++ PatternConverter tail; ++ protected FormattingInfo formattingInfo = new FormattingInfo(); ++ protected String pattern; ++ ++ public PatternParser(String pattern) { ++ this.pattern = pattern; ++ patternLength = pattern.length(); ++ state = LITERAL_STATE; + } +- } +- +- protected +- String extractOption() { +- if((i < patternLength) && (pattern.charAt(i) == '{')) { +- int end = pattern.indexOf('}', i); +- if (end > i) { +- String r = pattern.substring(i + 1, end); +- i = end+1; +- return r; +- } +- } +- return null; +- } +- +- +- /** +- The option is expected to be in decimal and positive. In case of +- error, zero is returned. */ +- protected +- int extractPrecisionOption() { +- String opt = extractOption(); +- int r = 0; +- if(opt != null) { +- try { +- r = Integer.parseInt(opt); +- if(r <= 0) { +- LogLog.error( +- "Precision option (" + opt + ") isn't a positive integer."); +- r = 0; ++ ++ private void addToList(PatternConverter pc) { ++ if (head == null) { ++ head = tail = pc; ++ } else { ++ tail.next = pc; ++ tail = pc; + } +- } +- catch (NumberFormatException e) { +- LogLog.error("Category option \""+opt+"\" not a decimal integer.", e); +- } + } +- return r; +- } +- +- public +- PatternConverter parse() { +- char c; +- i = 0; +- while(i < patternLength) { +- c = pattern.charAt(i++); +- switch(state) { +- case LITERAL_STATE: +- // In literal state, the last char is always a literal. +- if(i == patternLength) { +- currentLiteral.append(c); +- continue; +- } +- if(c == ESCAPE_CHAR) { +- // peek at the next char. +- switch(pattern.charAt(i)) { +- case ESCAPE_CHAR: +- currentLiteral.append(c); +- i++; // move pointer +- break; +- case 'n': +- currentLiteral.append(Layout.LINE_SEP); +- i++; // move pointer +- break; +- default: +- if(currentLiteral.length() != 0) { +- addToList(new LiteralPatternConverter( +- currentLiteral.toString())); +- //LogLog.debug("Parsed LITERAL converter: \"" +- // +currentLiteral+"\"."); +- } +- currentLiteral.setLength(0); +- currentLiteral.append(c); // append % +- state = CONVERTER_STATE; +- formattingInfo.reset(); +- } +- } +- else { +- currentLiteral.append(c); +- } +- break; +- case CONVERTER_STATE: +- currentLiteral.append(c); +- switch(c) { +- case '-': +- formattingInfo.leftAlign = true; +- break; +- case '.': +- state = DOT_STATE; +- break; +- default: +- if(c >= '0' && c <= '9') { +- formattingInfo.min = c - '0'; +- state = MIN_STATE; +- } +- else +- finalizeConverter(c); +- } // switch +- break; +- case MIN_STATE: +- currentLiteral.append(c); +- if(c >= '0' && c <= '9') +- formattingInfo.min = formattingInfo.min*10 + (c - '0'); +- else if(c == '.') +- state = DOT_STATE; +- else { +- finalizeConverter(c); +- } +- break; +- case DOT_STATE: +- currentLiteral.append(c); +- if(c >= '0' && c <= '9') { +- formattingInfo.max = c - '0'; +- state = MAX_STATE; +- } +- else { +- LogLog.error("Error occured in position "+i +- +".\n Was expecting digit, instead got char \""+c+"\"."); +- state = LITERAL_STATE; +- } +- break; +- case MAX_STATE: +- currentLiteral.append(c); +- if(c >= '0' && c <= '9') +- formattingInfo.max = formattingInfo.max*10 + (c - '0'); +- else { +- finalizeConverter(c); +- state = LITERAL_STATE; ++ ++ protected String extractOption() { ++ if ((i < patternLength) && (pattern.charAt(i) == LEFT_BRACKET)) { ++ int end = pattern.indexOf(RIGHT_BRACKET, i); ++ if (end > i) { ++ String r = pattern.substring(i + 1, end); ++ i = end + 1; ++ return r; ++ } + } +- break; +- } // switch +- } // while +- if(currentLiteral.length() != 0) { +- addToList(new LiteralPatternConverter(currentLiteral.toString())); +- //LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\"."); ++ return null; + } +- return head; +- } +- +- protected +- void finalizeConverter(char c) { +- PatternConverter pc = null; +- switch(c) { +- case 'c': +- pc = new CategoryPatternConverter(formattingInfo, +- extractPrecisionOption()); +- //LogLog.debug("CATEGORY converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'C': +- pc = new ClassNamePatternConverter(formattingInfo, +- extractPrecisionOption()); +- //LogLog.debug("CLASS_NAME converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'd': +- String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT; +- DateFormat df; +- String dOpt = extractOption(); +- if(dOpt != null) +- dateFormatStr = dOpt; +- +- if(dateFormatStr.equalsIgnoreCase( +- AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT)) +- df = new ISO8601DateFormat(); +- else if(dateFormatStr.equalsIgnoreCase( +- AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT)) +- df = new AbsoluteTimeDateFormat(); +- else if(dateFormatStr.equalsIgnoreCase( +- AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT)) +- df = new DateTimeDateFormat(); +- else { +- try { +- df = new SimpleDateFormat(dateFormatStr); +- } +- catch (IllegalArgumentException e) { +- LogLog.error("Could not instantiate SimpleDateFormat with " + +- dateFormatStr, e); +- df = (DateFormat) OptionConverter.instantiateByClassName( +- "org.apache.log4j.helpers.ISO8601DateFormat", +- DateFormat.class, null); ++ ++ /** ++ * The option is expected to be in decimal and positive. In case of error, zero ++ * is returned. ++ */ ++ protected int extractPrecisionOption() { ++ String opt = extractOption(); ++ int r = 0; ++ if (opt != null) { ++ try { ++ r = Integer.parseInt(opt); ++ if (r <= 0) { ++ LogLog.error("Precision option (" + opt + ") isn't a positive integer."); ++ r = 0; ++ } ++ } catch (NumberFormatException e) { ++ LogLog.error("Category option \"" + opt + "\" not a decimal integer.", e); ++ } + } +- } +- pc = new DatePatternConverter(formattingInfo, df); +- //LogLog.debug("DATE converter {"+dateFormatStr+"}."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'F': +- pc = new LocationPatternConverter(formattingInfo, +- FILE_LOCATION_CONVERTER); +- //LogLog.debug("File name converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'l': +- pc = new LocationPatternConverter(formattingInfo, +- FULL_LOCATION_CONVERTER); +- //LogLog.debug("Location converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'L': +- pc = new LocationPatternConverter(formattingInfo, +- LINE_LOCATION_CONVERTER); +- //LogLog.debug("LINE NUMBER converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'm': +- pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER); +- //LogLog.debug("MESSAGE converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'M': +- pc = new LocationPatternConverter(formattingInfo, +- METHOD_LOCATION_CONVERTER); +- //LogLog.debug("METHOD converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'p': +- pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER); +- //LogLog.debug("LEVEL converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 'r': +- pc = new BasicPatternConverter(formattingInfo, +- RELATIVE_TIME_CONVERTER); +- //LogLog.debug("RELATIVE time converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- case 't': +- pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER); +- //LogLog.debug("THREAD converter."); +- //formattingInfo.dump(); +- currentLiteral.setLength(0); +- break; +- /*case 'u': +- if(i < patternLength) { +- char cNext = pattern.charAt(i); +- if(cNext >= '0' && cNext <= '9') { +- pc = new UserFieldPatternConverter(formattingInfo, cNext - '0'); +- LogLog.debug("USER converter ["+cNext+"]."); +- formattingInfo.dump(); +- currentLiteral.setLength(0); +- i++; ++ return r; ++ } ++ ++ public PatternConverter parse() { ++ char c; ++ i = 0; ++ while (i < patternLength) { ++ c = pattern.charAt(i++); ++ switch (state) { ++ case LITERAL_STATE: ++ // In literal state, the last char is always a literal. ++ if (i == patternLength) { ++ currentLiteral.append(c); ++ continue; ++ } ++ if (c == ESCAPE_CHAR) { ++ // peek at the next char. ++ switch (pattern.charAt(i)) { ++ case ESCAPE_CHAR: ++ currentLiteral.append(c); ++ i++; // move pointer ++ break; ++ case N_CHAR: ++ currentLiteral.append(Layout.LINE_SEP); ++ i++; // move pointer ++ break; ++ default: ++ if (currentLiteral.length() != 0) { ++ addToList(new LiteralPatternConverter(currentLiteral.toString())); ++ // LogLog.debug("Parsed LITERAL converter: \"" ++ // +currentLiteral+"\"."); ++ } ++ currentLiteral.setLength(0); ++ currentLiteral.append(c); // append % ++ state = CONVERTER_STATE; ++ formattingInfo.reset(); ++ } ++ } else { ++ currentLiteral.append(c); ++ } ++ break; ++ case CONVERTER_STATE: ++ currentLiteral.append(c); ++ switch (c) { ++ case DASH_CHAR: ++ formattingInfo.leftAlign = true; ++ break; ++ case DOT_CHAR: ++ state = DOT_STATE; ++ break; ++ default: ++ if (c >= '0' && c <= '9') { ++ formattingInfo.min = c - '0'; ++ state = MIN_STATE; ++ } else ++ finalizeConverter(c); ++ } // switch ++ break; ++ case MIN_STATE: ++ currentLiteral.append(c); ++ if (c >= '0' && c <= '9') ++ formattingInfo.min = formattingInfo.min * 10 + (c - '0'); ++ else if (c == '.') ++ state = DOT_STATE; ++ else { ++ finalizeConverter(c); ++ } ++ break; ++ case DOT_STATE: ++ currentLiteral.append(c); ++ if (c >= '0' && c <= '9') { ++ formattingInfo.max = c - '0'; ++ state = MAX_STATE; ++ } else { ++ LogLog.error("Error occured in position " + i + ".\n Was expecting digit, instead got char \"" + c ++ + "\"."); ++ state = LITERAL_STATE; ++ } ++ break; ++ case MAX_STATE: ++ currentLiteral.append(c); ++ if (c >= '0' && c <= '9') ++ formattingInfo.max = formattingInfo.max * 10 + (c - '0'); ++ else { ++ finalizeConverter(c); ++ state = LITERAL_STATE; ++ } ++ break; ++ } // switch ++ } // while ++ if (currentLiteral.length() != 0) { ++ addToList(new LiteralPatternConverter(currentLiteral.toString())); ++ // LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\"."); + } +- else +- LogLog.error("Unexpected char" +cNext+" at position "+i); +- } +- break;*/ +- case 'x': +- pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER); +- //LogLog.debug("NDC converter."); +- currentLiteral.setLength(0); +- break; +- case 'X': +- String xOpt = extractOption(); +- pc = new MDCPatternConverter(formattingInfo, xOpt); +- currentLiteral.setLength(0); +- break; +- default: +- LogLog.error("Unexpected char [" +c+"] at position "+i +- +" in conversion patterrn."); +- pc = new LiteralPatternConverter(currentLiteral.toString()); +- currentLiteral.setLength(0); ++ return head; + } + +- addConverter(pc); +- } +- +- protected +- void addConverter(PatternConverter pc) { +- currentLiteral.setLength(0); +- // Add the pattern converter to the list. +- addToList(pc); +- // Next pattern is assumed to be a literal. +- state = LITERAL_STATE; +- // Reset formatting info +- formattingInfo.reset(); +- } +- +- // --------------------------------------------------------------------- +- // PatternConverters +- // --------------------------------------------------------------------- +- +- private static class BasicPatternConverter extends PatternConverter { +- int type; +- +- BasicPatternConverter(FormattingInfo formattingInfo, int type) { +- super(formattingInfo); +- this.type = type; ++ protected void finalizeConverter(char c) { ++ PatternConverter pc = null; ++ switch (c) { ++ case 'c': ++ pc = new CategoryPatternConverter(formattingInfo, extractPrecisionOption()); ++ // LogLog.debug("CATEGORY converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'C': ++ pc = new ClassNamePatternConverter(formattingInfo, extractPrecisionOption()); ++ // LogLog.debug("CLASS_NAME converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'd': ++ String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT; ++ DateFormat df; ++ String dOpt = extractOption(); ++ if (dOpt != null) ++ dateFormatStr = dOpt; ++ ++ if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT)) ++ df = new ISO8601DateFormat(); ++ else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT)) ++ df = new AbsoluteTimeDateFormat(); ++ else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT)) ++ df = new DateTimeDateFormat(); ++ else { ++ try { ++ df = new SimpleDateFormat(dateFormatStr); ++ } catch (IllegalArgumentException e) { ++ LogLog.error("Could not instantiate SimpleDateFormat with " + dateFormatStr, e); ++ df = (DateFormat) OptionConverter.instantiateByClassName( ++ "org.apache.log4j.helpers.ISO8601DateFormat", DateFormat.class, null); ++ } ++ } ++ pc = new DatePatternConverter(formattingInfo, df); ++ // LogLog.debug("DATE converter {"+dateFormatStr+"}."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'F': ++ pc = new LocationPatternConverter(formattingInfo, FILE_LOCATION_CONVERTER); ++ // LogLog.debug("File name converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'l': ++ pc = new LocationPatternConverter(formattingInfo, FULL_LOCATION_CONVERTER); ++ // LogLog.debug("Location converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'L': ++ pc = new LocationPatternConverter(formattingInfo, LINE_LOCATION_CONVERTER); ++ // LogLog.debug("LINE NUMBER converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'm': ++ pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER); ++ // LogLog.debug("MESSAGE converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'M': ++ pc = new LocationPatternConverter(formattingInfo, METHOD_LOCATION_CONVERTER); ++ // LogLog.debug("METHOD converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'p': ++ pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER); ++ // LogLog.debug("LEVEL converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 'r': ++ pc = new BasicPatternConverter(formattingInfo, RELATIVE_TIME_CONVERTER); ++ // LogLog.debug("RELATIVE time converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ case 't': ++ pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER); ++ // LogLog.debug("THREAD converter."); ++ // formattingInfo.dump(); ++ currentLiteral.setLength(0); ++ break; ++ /* ++ * case 'u': if(i < patternLength) { char cNext = pattern.charAt(i); if(cNext >= ++ * '0' && cNext <= '9') { pc = new UserFieldPatternConverter(formattingInfo, ++ * cNext - '0'); LogLog.debug("USER converter ["+cNext+"]."); ++ * formattingInfo.dump(); currentLiteral.setLength(0); i++; } else ++ * LogLog.error("Unexpected char" +cNext+" at position "+i); } break; ++ */ ++ case 'x': ++ pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER); ++ // LogLog.debug("NDC converter."); ++ currentLiteral.setLength(0); ++ break; ++ case 'X': ++ String xOpt = extractOption(); ++ pc = new MDCPatternConverter(formattingInfo, xOpt); ++ currentLiteral.setLength(0); ++ break; ++ default: ++ LogLog.error("Unexpected char [" + c + "] at position " + i + " in conversion patterrn."); ++ pc = new LiteralPatternConverter(currentLiteral.toString()); ++ currentLiteral.setLength(0); ++ } ++ ++ addConverter(pc); + } + +- public +- String convert(LoggingEvent event) { +- switch(type) { +- case RELATIVE_TIME_CONVERTER: +- return (Long.toString(event.timeStamp - LoggingEvent.getStartTime())); +- case THREAD_CONVERTER: +- return event.getThreadName(); +- case LEVEL_CONVERTER: +- return event.getLevel().toString(); +- case NDC_CONVERTER: +- return event.getNDC(); +- case MESSAGE_CONVERTER: { +- return event.getRenderedMessage(); +- } +- default: return null; +- } ++ protected void addConverter(PatternConverter pc) { ++ currentLiteral.setLength(0); ++ // Add the pattern converter to the list. ++ addToList(pc); ++ // Next pattern is assumed to be a literal. ++ state = LITERAL_STATE; ++ // Reset formatting info ++ formattingInfo.reset(); + } +- } + +- private static class LiteralPatternConverter extends PatternConverter { +- private String literal; ++ // --------------------------------------------------------------------- ++ // PatternConverters ++ // --------------------------------------------------------------------- + +- LiteralPatternConverter(String value) { +- literal = value; +- } ++ private static class BasicPatternConverter extends PatternConverter { ++ int type; + +- public +- final +- void format(StringBuffer sbuf, LoggingEvent event) { +- sbuf.append(literal); +- } ++ BasicPatternConverter(FormattingInfo formattingInfo, int type) { ++ super(formattingInfo); ++ this.type = type; ++ } + +- public +- String convert(LoggingEvent event) { +- return literal; ++ public String convert(LoggingEvent event) { ++ switch (type) { ++ case RELATIVE_TIME_CONVERTER: ++ return (Long.toString(event.timeStamp - LoggingEvent.getStartTime())); ++ case THREAD_CONVERTER: ++ return event.getThreadName(); ++ case LEVEL_CONVERTER: ++ return event.getLevel().toString(); ++ case NDC_CONVERTER: ++ return event.getNDC(); ++ case MESSAGE_CONVERTER: { ++ return event.getRenderedMessage(); ++ } ++ default: ++ return null; ++ } ++ } + } +- } + +- private static class DatePatternConverter extends PatternConverter { +- private DateFormat df; +- private Date date; ++ private static class LiteralPatternConverter extends PatternConverter { ++ private String literal; + +- DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) { +- super(formattingInfo); +- date = new Date(); +- this.df = df; +- } ++ LiteralPatternConverter(String value) { ++ literal = value; ++ } + +- public +- String convert(LoggingEvent event) { +- date.setTime(event.timeStamp); +- String converted = null; +- try { +- converted = df.format(date); +- } +- catch (Exception ex) { +- LogLog.error("Error occured while converting date.", ex); +- } +- return converted; ++ public final void format(StringBuffer sbuf, LoggingEvent event) { ++ sbuf.append(literal); ++ } ++ ++ public String convert(LoggingEvent event) { ++ return literal; ++ } + } +- } + +- private static class MDCPatternConverter extends PatternConverter { +- private String key; ++ private static class DatePatternConverter extends PatternConverter { ++ private DateFormat df; ++ private Date date; + +- MDCPatternConverter(FormattingInfo formattingInfo, String key) { +- super(formattingInfo); +- this.key = key; +- } ++ DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) { ++ super(formattingInfo); ++ date = new Date(); ++ this.df = df; ++ } + +- public +- String convert(LoggingEvent event) { +- if (key == null) { +- StringBuffer buf = new StringBuffer("{"); +- Map properties = event.getProperties(); +- if (properties.size() > 0) { +- Object[] keys = properties.keySet().toArray(); +- Arrays.sort(keys); +- for (int i = 0; i < keys.length; i++) { +- buf.append('{'); +- buf.append(keys[i]); +- buf.append(','); +- buf.append(properties.get(keys[i])); +- buf.append('}'); +- } +- } +- buf.append('}'); +- return buf.toString(); +- } else { +- Object val = event.getMDC(key); +- if(val == null) { +- return null; +- } else { +- return val.toString(); +- } +- } ++ public String convert(LoggingEvent event) { ++ date.setTime(event.timeStamp); ++ String converted = null; ++ try { ++ converted = df.format(date); ++ } catch (Exception ex) { ++ LogLog.error("Error occured while converting date.", ex); ++ } ++ return converted; ++ } + } +- } + ++ private static class MDCPatternConverter extends PatternConverter { ++ private String key; + +- private class LocationPatternConverter extends PatternConverter { +- int type; ++ MDCPatternConverter(FormattingInfo formattingInfo, String key) { ++ super(formattingInfo); ++ this.key = key; ++ } + +- LocationPatternConverter(FormattingInfo formattingInfo, int type) { +- super(formattingInfo); +- this.type = type; ++ public String convert(LoggingEvent event) { ++ if (key == null) { ++ StringBuffer buf = new StringBuffer("{"); ++ Map properties = event.getProperties(); ++ if (properties.size() > 0) { ++ Object[] keys = properties.keySet().toArray(); ++ Arrays.sort(keys); ++ for (int i = 0; i < keys.length; i++) { ++ buf.append('{'); ++ buf.append(keys[i]); ++ buf.append(','); ++ buf.append(properties.get(keys[i])); ++ buf.append('}'); ++ } ++ } ++ buf.append('}'); ++ return buf.toString(); ++ } else { ++ Object val = event.getMDC(key); ++ if (val == null) { ++ return null; ++ } else { ++ return val.toString(); ++ } ++ } ++ } + } + +- public +- String convert(LoggingEvent event) { +- LocationInfo locationInfo = event.getLocationInformation(); +- switch(type) { +- case FULL_LOCATION_CONVERTER: +- return locationInfo.fullInfo; +- case METHOD_LOCATION_CONVERTER: +- return locationInfo.getMethodName(); +- case LINE_LOCATION_CONVERTER: +- return locationInfo.getLineNumber(); +- case FILE_LOCATION_CONVERTER: +- return locationInfo.getFileName(); +- default: return null; +- } +- } +- } ++ private class LocationPatternConverter extends PatternConverter { ++ int type; + +- private static abstract class NamedPatternConverter extends PatternConverter { +- int precision; ++ LocationPatternConverter(FormattingInfo formattingInfo, int type) { ++ super(formattingInfo); ++ this.type = type; ++ } + +- NamedPatternConverter(FormattingInfo formattingInfo, int precision) { +- super(formattingInfo); +- this.precision = precision; ++ public String convert(LoggingEvent event) { ++ LocationInfo locationInfo = event.getLocationInformation(); ++ switch (type) { ++ case FULL_LOCATION_CONVERTER: ++ return locationInfo.fullInfo; ++ case METHOD_LOCATION_CONVERTER: ++ return locationInfo.getMethodName(); ++ case LINE_LOCATION_CONVERTER: ++ return locationInfo.getLineNumber(); ++ case FILE_LOCATION_CONVERTER: ++ return locationInfo.getFileName(); ++ default: ++ return null; ++ } ++ } + } + +- abstract +- String getFullyQualifiedName(LoggingEvent event); +- +- public +- String convert(LoggingEvent event) { +- String n = getFullyQualifiedName(event); +- if(precision <= 0) +- return n; +- else { +- int len = n.length(); +- +- // We substract 1 from 'len' when assigning to 'end' to avoid out of +- // bounds exception in return r.substring(end+1, len). This can happen if +- // precision is 1 and the category name ends with a dot. +- int end = len -1 ; +- for(int i = precision; i > 0; i--) { +- end = n.lastIndexOf('.', end-1); +- if(end == -1) +- return n; ++ private static abstract class NamedPatternConverter extends PatternConverter { ++ int precision; ++ ++ NamedPatternConverter(FormattingInfo formattingInfo, int precision) { ++ super(formattingInfo); ++ this.precision = precision; ++ } ++ ++ abstract String getFullyQualifiedName(LoggingEvent event); ++ ++ public String convert(LoggingEvent event) { ++ String n = getFullyQualifiedName(event); ++ if (precision <= 0) ++ return n; ++ else { ++ int len = n.length(); ++ ++ // We substract 1 from 'len' when assigning to 'end' to avoid out of ++ // bounds exception in return r.substring(end+1, len). This can happen if ++ // precision is 1 and the category name ends with a dot. ++ int end = len - 1; ++ for (int i = precision; i > 0; i--) { ++ end = n.lastIndexOf('.', end - 1); ++ if (end == -1) ++ return n; ++ } ++ return n.substring(end + 1, len); ++ } + } +- return n.substring(end+1, len); +- } + } +- } + +- private class ClassNamePatternConverter extends NamedPatternConverter { ++ private class ClassNamePatternConverter extends NamedPatternConverter { + +- ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) { +- super(formattingInfo, precision); +- } ++ ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) { ++ super(formattingInfo, precision); ++ } + +- String getFullyQualifiedName(LoggingEvent event) { +- return event.getLocationInformation().getClassName(); ++ String getFullyQualifiedName(LoggingEvent event) { ++ return event.getLocationInformation().getClassName(); ++ } + } +- } + +- private class CategoryPatternConverter extends NamedPatternConverter { ++ private class CategoryPatternConverter extends NamedPatternConverter { + +- CategoryPatternConverter(FormattingInfo formattingInfo, int precision) { +- super(formattingInfo, precision); +- } ++ CategoryPatternConverter(FormattingInfo formattingInfo, int precision) { ++ super(formattingInfo, precision); ++ } + +- String getFullyQualifiedName(LoggingEvent event) { +- return event.getLoggerName(); ++ String getFullyQualifiedName(LoggingEvent event) { ++ return event.getLoggerName(); ++ } + } +- } + } +- +diff --git a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java +index ad35f657..6ba9b66c 100644 +--- a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java ++++ b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java +@@ -18,381 +18,414 @@ package org.apache.log4j.jdbc; + + import java.sql.Connection; + import java.sql.DriverManager; ++import java.sql.PreparedStatement; + import java.sql.SQLException; + import java.sql.Statement; + import java.util.ArrayList; +-import java.util.Iterator; + + import org.apache.log4j.PatternLayout; ++import org.apache.log4j.helpers.LogLog; + import org.apache.log4j.spi.ErrorCode; + import org.apache.log4j.spi.LoggingEvent; + +- + /** +- The JDBCAppender provides for sending log events to a database. +- +-

WARNING: This version of JDBCAppender +- is very likely to be completely replaced in the future. Moreoever, +- it does not log exceptions. +- +-

Each append call adds to an ArrayList buffer. When +- the buffer is filled each log event is placed in a sql statement +- (configurable) and executed. +- +- BufferSize, db URL, User, & Password are +- configurable options in the standard log4j ways. +- +-

The setSql(String sql) sets the SQL statement to be +- used for logging -- this statement is sent to a +- PatternLayout (either created automaticly by the +- appender or added by the user). Therefore by default all the +- conversion patterns in PatternLayout can be used +- inside of the statement. (see the test cases for examples) +- +-

Overriding the {@link #getLogStatement} method allows more +- explicit control of the statement used for logging. +- +-

For use as a base class: +- +-

    +- +-
  • Override getConnection() to pass any connection +- you want. Typically this is used to enable application wide +- connection pooling. +- +-
  • Override closeConnection(Connection con) -- if +- you override getConnection make sure to implement +- closeConnection to handle the connection you +- generated. Typically this would return the connection to the +- pool it came from. +- +-
  • Override getLogStatement(LoggingEvent event) to +- produce specialized or dynamic statements. The default uses the +- sql option value. +- +-
+- +- @author Kevin Steppe (ksteppe@pacbell.net) +- +-*/ +-public class JDBCAppender extends org.apache.log4j.AppenderSkeleton +- implements org.apache.log4j.Appender { +- +- /** +- * URL of the DB for default connection handling +- */ +- protected String databaseURL = "jdbc:odbc:myDB"; +- +- /** +- * User to connect as for default connection handling +- */ +- protected String databaseUser = "me"; +- +- /** +- * User to use for default connection handling +- */ +- protected String databasePassword = "mypassword"; +- +- /** +- * Connection used by default. The connection is opened the first time it +- * is needed and then held open until the appender is closed (usually at +- * garbage collection). This behavior is best modified by creating a +- * sub-class and overriding the getConnection and +- * closeConnection methods. +- */ +- protected Connection connection = null; +- +- /** +- * Stores the string given to the pattern layout for conversion into a SQL +- * statement, eg: insert into LogTable (Thread, Class, Message) values +- * ("%t", "%c", "%m"). +- * +- * Be careful of quotes in your messages! +- * +- * Also see PatternLayout. +- */ +- protected String sqlStatement = ""; +- +- /** +- * size of LoggingEvent buffer before writting to the database. +- * Default is 1. +- */ +- protected int bufferSize = 1; +- +- /** +- * ArrayList holding the buffer of Logging Events. +- */ +- protected ArrayList buffer; +- +- /** +- * Helper object for clearing out the buffer +- */ +- protected ArrayList removes; +- +- private boolean locationInfo = false; +- +- public JDBCAppender() { +- super(); +- buffer = new ArrayList(bufferSize); +- removes = new ArrayList(bufferSize); +- } +- +- /** +- * Gets whether the location of the logging request call +- * should be captured. +- * +- * @since 1.2.16 +- * @return the current value of the LocationInfo option. +- */ +- public boolean getLocationInfo() { +- return locationInfo; +- } +- +- /** +- * The LocationInfo option takes a boolean value. By default, it is +- * set to false which means there will be no effort to extract the location +- * information related to the event. As a result, the event that will be +- * ultimately logged will likely to contain the wrong location information +- * (if present in the log format). +- *

+- *

+- * Location information extraction is comparatively very slow and should be +- * avoided unless performance is not a concern. +- *

+- * @since 1.2.16 +- * @param flag true if location information should be extracted. +- */ +- public void setLocationInfo(final boolean flag) { +- locationInfo = flag; +- } +- +- +- /** +- * Adds the event to the buffer. When full the buffer is flushed. +- */ +- public void append(LoggingEvent event) { +- event.getNDC(); +- event.getThreadName(); +- // Get a copy of this thread's MDC. +- event.getMDCCopy(); +- if (locationInfo) { +- event.getLocationInformation(); +- } +- event.getRenderedMessage(); +- event.getThrowableStrRep(); +- buffer.add(event); +- +- if (buffer.size() >= bufferSize) +- flushBuffer(); +- } +- +- /** +- * By default getLogStatement sends the event to the required Layout object. +- * The layout will format the given pattern into a workable SQL string. +- * +- * Overriding this provides direct access to the LoggingEvent +- * when constructing the logging statement. +- * +- */ +- protected String getLogStatement(LoggingEvent event) { +- return getLayout().format(event); +- } +- +- /** +- * +- * Override this to provide an alertnate method of getting +- * connections (such as caching). One method to fix this is to open +- * connections at the start of flushBuffer() and close them at the +- * end. I use a connection pool outside of JDBCAppender which is +- * accessed in an override of this method. +- * */ +- protected void execute(String sql) throws SQLException { +- +- Connection con = null; +- Statement stmt = null; +- +- try { +- con = getConnection(); +- +- stmt = con.createStatement(); +- stmt.executeUpdate(sql); +- } finally { +- if(stmt != null) { +- stmt.close(); +- } +- closeConnection(con); ++ * The JDBCAppender provides for sending log events to a database. ++ * ++ *

++ * WARNING: This version of JDBCAppender is very likely ++ * to be completely replaced in the future. Moreoever, it does not log ++ * exceptions. ++ * ++ *

++ * Each append call adds to an ArrayList buffer. When the buffer is ++ * filled each log event is placed in a sql statement (configurable) and ++ * executed. ++ * ++ * BufferSize, db URL, User, & Password are ++ * configurable options in the standard log4j ways. ++ * ++ *

++ * The setSql(String sql) sets the SQL statement to be used for ++ * logging -- this statement is sent to a PatternLayout (either ++ * created automatically by the appender or added by the user). Therefore by ++ * default all the conversion patterns in PatternLayout can be used ++ * inside of the statement. (see the test cases for examples) ++ * ++ *

++ * Overriding the {@link #getLogStatement} method allows more explicit control ++ * of the statement used for logging. ++ * ++ *

++ * For use as a base class: ++ * ++ *

    ++ * ++ *
  • Override getConnection() to pass any connection you want. ++ * Typically this is used to enable application wide connection pooling. ++ * ++ *
  • Override closeConnection(Connection con) -- if you override ++ * getConnection make sure to implement closeConnection to handle ++ * the connection you generated. Typically this would return the connection to ++ * the pool it came from. ++ * ++ *
  • Override getLogStatement(LoggingEvent event) to produce ++ * specialized or dynamic statements. The default uses the sql option value. ++ * ++ *
++ * ++ * @author Kevin Steppe ++ * (ksteppe@pacbell.net) ++ * ++ */ ++public class JDBCAppender extends org.apache.log4j.AppenderSkeleton implements org.apache.log4j.Appender { ++ ++// private final Boolean secureSqlReplacement = Boolean ++// .parseBoolean(System.getProperty("org.apache.log4j.jdbc.JDBCAppender.secure_jdbc_replacement", "true")); ++ ++ private static final IllegalArgumentException ILLEGAL_PATTERN_FOR_SECURE_EXEC = new IllegalArgumentException( ++ "Only org.apache.log4j.PatternLayout is supported for now due to CVE-2022-23305"); ++ ++ /** ++ * URL of the DB for default connection handling ++ */ ++ protected String databaseURL = "jdbc:odbc:myDB"; ++ ++ /** ++ * User to connect as for default connection handling ++ */ ++ protected String databaseUser = "me"; ++ ++ /** ++ * User to use for default connection handling ++ */ ++ protected String databasePassword = "mypassword"; ++ ++ /** ++ * Connection used by default. The connection is opened the first time it is ++ * needed and then held open until the appender is closed (usually at garbage ++ * collection). This behavior is best modified by creating a sub-class and ++ * overriding the getConnection and closeConnection ++ * methods. ++ */ ++ protected Connection connection = null; ++ ++ /** ++ * Stores the string given to the pattern layout for conversion into a SQL ++ * statement, eg: insert into LogTable (Thread, Class, Message) values ("%t", ++ * "%c", "%m"). ++ * ++ * Be careful of quotes in your messages! ++ * ++ * Also see PatternLayout. ++ */ ++ protected String sqlStatement = ""; ++ ++ private JdbcPatternParser preparedStatementParser; ++ /** ++ * size of LoggingEvent buffer before writting to the database. Default is 1. ++ */ ++ protected int bufferSize = 1; ++ ++ /** ++ * ArrayList holding the buffer of Logging Events. ++ */ ++ protected ArrayList buffer; ++ ++ private boolean locationInfo = false; ++ ++ private boolean isActive = false; ++ ++ public JDBCAppender() { ++ super(); ++ buffer = new ArrayList(bufferSize); + } + +- //System.out.println("Execute: " + sql); +- } +- +- +- /** +- * Override this to return the connection to a pool, or to clean up the +- * resource. +- * +- * The default behavior holds a single connection open until the appender +- * is closed (typically when garbage collected). +- */ +- protected void closeConnection(Connection con) { +- } +- +- /** +- * Override this to link with your connection pooling system. +- * +- * By default this creates a single connection which is held open +- * until the object is garbage collected. +- */ +- protected Connection getConnection() throws SQLException { +- if (!DriverManager.getDrivers().hasMoreElements()) +- setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); +- +- if (connection == null) { +- connection = DriverManager.getConnection(databaseURL, databaseUser, +- databasePassword); +- } +- +- return connection; +- } +- +- /** +- * Closes the appender, flushing the buffer first then closing the default +- * connection if it is open. +- */ +- public void close() +- { +- flushBuffer(); +- +- try { +- if (connection != null && !connection.isClosed()) +- connection.close(); +- } catch (SQLException e) { +- errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); ++ @Override ++ public void activateOptions() { ++ ++ if (getSql() == null || getSql().trim().length() == 0) { ++ LogLog.error("JDBCAppender.sql parameter is mandatory. Skipping all futher processing"); ++ isActive = false; ++ return; ++ } ++ ++ LogLog.debug("JDBCAppender constructing internal pattern parser"); ++ preparedStatementParser = new JdbcPatternParser(getSql()); ++ isActive = true; + } +- this.closed = true; +- } +- +- /** +- * loops through the buffer of LoggingEvents, gets a +- * sql string from getLogStatement() and sends it to execute(). +- * Errors are sent to the errorHandler. +- * +- * If a statement fails the LoggingEvent stays in the buffer! +- */ +- public void flushBuffer() { +- //Do the actual logging +- removes.ensureCapacity(buffer.size()); +- for (Iterator i = buffer.iterator(); i.hasNext();) { +- LoggingEvent logEvent = (LoggingEvent)i.next(); +- try { +- String sql = getLogStatement(logEvent); +- execute(sql); +- } +- catch (SQLException e) { +- errorHandler.error("Failed to excute sql", e, +- ErrorCode.FLUSH_FAILURE); +- } finally { +- removes.add(logEvent); +- } ++ ++ /** ++ * Gets whether the location of the logging request call should be captured. ++ * ++ * @since 1.2.16 ++ * @return the current value of the LocationInfo option. ++ */ ++ public boolean getLocationInfo() { ++ return locationInfo; + } +- +- // remove from the buffer any events that were reported +- buffer.removeAll(removes); +- +- // clear the buffer of reported events +- removes.clear(); +- } +- +- +- /** closes the appender before disposal */ +- public void finalize() { +- close(); +- } +- +- +- /** +- * JDBCAppender requires a layout. +- * */ +- public boolean requiresLayout() { +- return true; +- } +- +- +- /** +- * +- */ +- public void setSql(String s) { +- sqlStatement = s; +- if (getLayout() == null) { +- this.setLayout(new PatternLayout(s)); ++ ++ /** ++ * The LocationInfo option takes a boolean value. By default, it is set ++ * to false which means there will be no effort to extract the location ++ * information related to the event. As a result, the event that will be ++ * ultimately logged will likely to contain the wrong location information (if ++ * present in the log format). ++ *

++ *

++ * Location information extraction is comparatively very slow and should be ++ * avoided unless performance is not a concern. ++ *

++ * ++ * @since 1.2.16 ++ * @param flag true if location information should be extracted. ++ */ ++ public void setLocationInfo(final boolean flag) { ++ locationInfo = flag; + } +- else { +- ((PatternLayout)getLayout()).setConversionPattern(s); ++ ++ /** ++ * Adds the event to the buffer. When full the buffer is flushed. ++ */ ++ public void append(LoggingEvent event) { ++ if (!isActive) { ++ return; ++ } ++ event.getNDC(); ++ event.getThreadName(); ++ // Get a copy of this thread's MDC. ++ event.getMDCCopy(); ++ if (locationInfo) { ++ event.getLocationInformation(); ++ } ++ event.getRenderedMessage(); ++ event.getThrowableStrRep(); ++ buffer.add(event); ++ ++ if (buffer.size() >= bufferSize) ++ flushBuffer(); + } +- } + ++ /** ++ * By default getLogStatement sends the event to the required Layout object. The ++ * layout will format the given pattern into a workable SQL string. ++ * ++ * Overriding this provides direct access to the LoggingEvent when constructing ++ * the logging statement. ++ * ++ */ ++ protected String getLogStatement(LoggingEvent event) { ++ return getLayout().format(event); ++ } + +- /** +- * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m") +- */ +- public String getSql() { +- return sqlStatement; +- } ++ /** ++ * ++ * Override this to provide an alertnate method of getting connections (such as ++ * caching). One method to fix this is to open connections at the start of ++ * flushBuffer() and close them at the end. I use a connection pool outside of ++ * JDBCAppender which is accessed in an override of this method. ++ */ ++ protected void execute(String sql) throws SQLException { ++ ++ Connection con = null; ++ Statement stmt = null; ++ ++ try { ++ con = getConnection(); ++ ++ stmt = con.createStatement(); ++ stmt.executeUpdate(sql); ++ } finally { ++ if (stmt != null) { ++ stmt.close(); ++ } ++ closeConnection(con); ++ } ++ ++ // System.out.println("Execute: " + sql); ++ } + ++ /** ++ * Override this to return the connection to a pool, or to clean up the ++ * resource. ++ * ++ * The default behavior holds a single connection open until the appender is ++ * closed (typically when garbage collected). ++ */ ++ protected void closeConnection(Connection con) { ++ } + +- public void setUser(String user) { +- databaseUser = user; +- } ++ /** ++ * Override this to link with your connection pooling system. ++ * ++ * By default this creates a single connection which is held open until the ++ * object is garbage collected. ++ */ ++ protected Connection getConnection() throws SQLException { ++ if (!DriverManager.getDrivers().hasMoreElements()) ++ setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); ++ ++ if (connection == null) { ++ connection = DriverManager.getConnection(databaseURL, databaseUser, databasePassword); ++ } ++ ++ return connection; ++ } + ++ /** ++ * Closes the appender, flushing the buffer first then closing the default ++ * connection if it is open. ++ */ ++ public void close() { ++ flushBuffer(); ++ ++ try { ++ if (connection != null && !connection.isClosed()) ++ connection.close(); ++ } catch (SQLException e) { ++ errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); ++ } ++ this.closed = true; ++ } + +- public void setURL(String url) { +- databaseURL = url; +- } ++ /** ++ * loops through the buffer of LoggingEvents, gets a sql string from ++ * getLogStatement() and sends it to execute(). Errors are sent to the ++ * errorHandler. ++ * ++ * If a statement fails the LoggingEvent stays in the buffer! ++ */ ++ public void flushBuffer() { ++ if (buffer.isEmpty()) { ++ return; ++ } ++ flushBufferSecure(); ++ } + ++ private void flushBufferSecure() { ++ // Prepare events that we will store to the DB ++ ArrayList removes = new ArrayList(buffer); ++ buffer.removeAll(removes); ++ ++ if (layout.getClass() != PatternLayout.class) { ++ errorHandler.error("Failed to convert pattern " + layout + " to SQL", ILLEGAL_PATTERN_FOR_SECURE_EXEC, ++ ErrorCode.MISSING_LAYOUT); ++ return; ++ } ++ Connection con = null; ++ boolean useBatch = removes.size() > 1; ++ try { ++ con = getConnection(); ++ PreparedStatement ps = con.prepareStatement(preparedStatementParser.getParameterizedSql()); ++ safelyInsertIntoDB(removes, useBatch, ps); ++ } catch (SQLException e) { ++ errorHandler.error("Failed to append messages sql", e, ErrorCode.FLUSH_FAILURE); ++ } finally { ++ if (con != null) { ++ closeConnection(con); ++ } ++ } ++ ++ } ++ ++ private void safelyInsertIntoDB(ArrayList removes, boolean useBatch, PreparedStatement ps) ++ throws SQLException { ++ try { ++ for (LoggingEvent logEvent : removes) { ++ try { ++ preparedStatementParser.setParameters(ps, logEvent); ++ if (useBatch) { ++ ps.addBatch(); ++ } ++ } catch (SQLException e) { ++ errorHandler.error("Failed to append parameters", e, ErrorCode.FLUSH_FAILURE); ++ } ++ } ++ if (useBatch) { ++ ps.executeBatch(); ++ } else { ++ ps.execute(); ++ } ++ } finally { ++ try { ++ ps.close(); ++ } catch (SQLException ignored) { ++ } ++ } ++ } + +- public void setPassword(String password) { +- databasePassword = password; +- } ++ /** closes the appender before disposal */ ++ public void finalize() { ++ close(); ++ } + ++ /** ++ * JDBCAppender requires a layout. ++ */ ++ public boolean requiresLayout() { ++ return true; ++ } + +- public void setBufferSize(int newBufferSize) { +- bufferSize = newBufferSize; +- buffer.ensureCapacity(bufferSize); +- removes.ensureCapacity(bufferSize); +- } ++ /** ++ * ++ */ ++ public void setSql(String s) { ++ sqlStatement = s; ++ if (getLayout() == null) { ++ this.setLayout(new PatternLayout(s)); ++ } else { ++ ((PatternLayout) getLayout()).setConversionPattern(s); ++ } ++ } + ++ /** ++ * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m") ++ */ ++ public String getSql() { ++ return sqlStatement; ++ } + +- public String getUser() { +- return databaseUser; +- } ++ public void setUser(String user) { ++ databaseUser = user; ++ } + ++ public void setURL(String url) { ++ databaseURL = url; ++ } + +- public String getURL() { +- return databaseURL; +- } ++ public void setPassword(String password) { ++ databasePassword = password; ++ } + ++ public void setBufferSize(int newBufferSize) { ++ bufferSize = newBufferSize; ++ buffer.ensureCapacity(bufferSize); ++ } + +- public String getPassword() { +- return databasePassword; +- } ++ public String getUser() { ++ return databaseUser; ++ } + ++ public String getURL() { ++ return databaseURL; ++ } + +- public int getBufferSize() { +- return bufferSize; +- } ++ public String getPassword() { ++ return databasePassword; ++ } + ++ public int getBufferSize() { ++ return bufferSize; ++ } + +- /** +- * Ensures that the given driver class has been loaded for sql connection +- * creation. +- */ +- public void setDriver(String driverClass) { +- try { +- Class.forName(driverClass); +- } catch (Exception e) { +- errorHandler.error("Failed to load driver", e, +- ErrorCode.GENERIC_FAILURE); ++ /** ++ * Ensures that the given driver class has been loaded for sql connection ++ * creation. ++ */ ++ public void setDriver(String driverClass) { ++ try { ++ Class.forName(driverClass); ++ } catch (Exception e) { ++ errorHandler.error("Failed to load driver", e, ErrorCode.GENERIC_FAILURE); ++ } + } +- } + } +- +diff --git a/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java +new file mode 100644 +index 00000000..d6807dee +--- /dev/null ++++ b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java +@@ -0,0 +1,115 @@ ++/* ++ * 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 org.apache.log4j.jdbc; ++ ++import org.apache.log4j.helpers.PatternConverter; ++import org.apache.log4j.helpers.PatternParser; ++import org.apache.log4j.spi.LoggingEvent; ++ ++import java.sql.PreparedStatement; ++import java.sql.SQLException; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.List; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++ ++class JdbcPatternParser { ++ private static final String QUESTION_MARK = "?"; ++ ++ private static final char PERCENT_CHAR = '%'; ++ ++ private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'((?>[^']|'')+)'"); ++ // NOTE: capturing group work seem to work just as well. ++ //private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'(([^']|'')+)'"); ++ ++ private String parameterizedSql; ++ final private List patternStringRepresentationList = new ArrayList(); ++ final private List args = new ArrayList(); ++ ++ JdbcPatternParser(String insertString) { ++ init(insertString); ++ } ++ ++ public String getParameterizedSql() { ++ return parameterizedSql; ++ } ++ ++ public List getUnmodifiablePatternStringRepresentationList() { ++ return Collections.unmodifiableList(patternStringRepresentationList); ++ } ++ ++ @Override ++ public String toString() { ++ return "JdbcPatternParser{sql=" + parameterizedSql + ",args=" + patternStringRepresentationList + "}"; ++ } ++ ++ /** ++ * Converts '....' literals into bind variables in JDBC. ++ */ ++ private void init(String insertString) { ++ if (insertString == null) { ++ throw new IllegalArgumentException("Null pattern"); ++ } ++ ++ Matcher m = STRING_LITERAL_PATTERN.matcher(insertString); ++ StringBuffer sb = new StringBuffer(); ++ while (m.find()) { ++ String matchedStr = m.group(1); ++ if (matchedStr.indexOf(PERCENT_CHAR) == -1) { ++ replaceWithMatchedStr(m, sb); ++ } else { ++ // Replace with bind ++ replaceWithBind(m, sb, matchedStr); ++ } ++ } ++ m.appendTail(sb); ++ this.parameterizedSql = sb.toString(); ++ } ++ ++ private void replaceWithMatchedStr(Matcher m, StringBuffer sb) { ++ // Just literal, append it as is ++ m.appendReplacement(sb, "'$1'"); ++ } ++ ++ private void replaceWithBind(Matcher m, StringBuffer sb, String matchedStr) { ++ m.appendReplacement(sb, QUESTION_MARK); ++ // We will use prepared statements, so we don't need to escape quotes. ++ // And we assume the users had 'That''s a string with quotes' in their configs. ++ matchedStr = matchedStr.replaceAll("''", "'"); ++ patternStringRepresentationList.add(matchedStr); ++ args.add(new PatternParser(matchedStr).parse()); ++ } ++ ++ public void setParameters(PreparedStatement ps, LoggingEvent logEvent) throws SQLException { ++ for (int i = 0; i < args.size(); i++) { ++ final PatternConverter head = args.get(i); ++ String value = buildValueStr(logEvent, head); ++ ps.setString(i + 1, value); ++ } ++ } ++ ++ private String buildValueStr(LoggingEvent logEvent, final PatternConverter head) { ++ StringBuffer buffer = new StringBuffer(); ++ PatternConverter c = head; ++ while (c != null) { ++ c.format(buffer, logEvent); ++ c = c.next; ++ } ++ return buffer.toString(); ++ } ++} +-- +2.33.1 + diff --git a/SOURCES/0001-Fix-CVE-2022-23307-Chainsaw.patch b/SOURCES/0001-Fix-CVE-2022-23307-Chainsaw.patch new file mode 100644 index 0000000..65cb04d --- /dev/null +++ b/SOURCES/0001-Fix-CVE-2022-23307-Chainsaw.patch @@ -0,0 +1,35 @@ +From 637fb986311f8c5a22cfb2ad2a6b928d179ea49c Mon Sep 17 00:00:00 2001 +From: Mikolaj Izdebski +Date: Wed, 2 Feb 2022 19:37:17 +0100 +Subject: [PATCH] Fix CVE-2022-23307 Chainsaw + +--- + src/main/java/org/apache/log4j/chainsaw/LoggingReceiver.java | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/main/java/org/apache/log4j/chainsaw/LoggingReceiver.java b/src/main/java/org/apache/log4j/chainsaw/LoggingReceiver.java +index ca087adc..7e739df5 100644 +--- a/src/main/java/org/apache/log4j/chainsaw/LoggingReceiver.java ++++ b/src/main/java/org/apache/log4j/chainsaw/LoggingReceiver.java +@@ -22,6 +22,8 @@ import java.io.ObjectInputStream; + import java.net.ServerSocket; + import java.net.Socket; + import java.net.SocketException; ++ ++import org.apache.log4j.FilteredObjectInputStream; + import org.apache.log4j.Logger; + import org.apache.log4j.spi.LoggingEvent; + +@@ -59,7 +61,8 @@ class LoggingReceiver extends Thread { + LOG.debug("Starting to get data"); + try { + final ObjectInputStream ois = +- new ObjectInputStream(mClient.getInputStream()); ++ new FilteredObjectInputStream( ++ mClient.getInputStream(), FilteredObjectInputStream.SYSTEM_ALLOWED_CLASSES); + while (true) { + final LoggingEvent event = (LoggingEvent) ois.readObject(); + mModel.addEvent(new EventDetails(event)); +-- +2.33.1 + diff --git a/SPECS/log4j.spec b/SPECS/log4j.spec index 6a2f06b..6ddb8ce 100644 --- a/SPECS/log4j.spec +++ b/SPECS/log4j.spec @@ -3,7 +3,7 @@ Name: log4j Version: 1.2.17 -Release: 17%{?dist} +Release: 18%{?dist} Epoch: 0 Summary: Java logging package BuildArch: noarch @@ -26,6 +26,9 @@ Patch5: 0012-Add-proper-bundle-symbolicname.patch Patch6: 0001-Backport-fix-for-CVE-2017-5645.patch Patch7: 0001-Add-test-case-for-JNDI-disablement.patch Patch8: 0002-Disable-JNDI-by-default.patch +Patch9: 0001-Fix-CVE-2022-23302-JMSSink.patch +Patch10: 0001-Fix-CVE-2022-23305-JDBCAppender.patch +Patch11: 0001-Fix-CVE-2022-23307-Chainsaw.patch BuildRequires: %{__perl} BuildRequires: maven-local @@ -65,6 +68,9 @@ Summary: API documentation for %{name} %patch6 -p1 -b .cve-2017-5645 %patch7 -p1 -b .log4shell %patch8 -p1 -b .log4shell +%patch9 -p1 -b .jms-cve +%patch10 -p1 -b .jdbc-cve +%patch11 -p1 -b .chainsaw-cve %pom_remove_plugin :maven-site-plugin sed -i "s|groupId>ant<|groupId>org.apache.ant<|g" pom.xml @@ -170,6 +176,12 @@ fi %changelog +* Wed Feb 02 2022 Mikolaj Izdebski - 0:1.2.17-18 +- Fix Unsafe deserialization flaw in Chainsaw log viewer +- Fix SQL injection when application is configured to use JDBCAppender +- Fix remote code execution when application is configured to use JMSSink +- Resolves: CVE-2022-23307, CVE-2022-23305, CVE-2022-23302 + * Wed Dec 15 2021 Mikolaj Izdebski - 0:1.2.17-17 - Fix remote code execution vulnerability - Resolves: CVE-2021-4104