org.jwall.apache.httpd.config.ConfigParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apache-config Show documentation
Show all versions of apache-config Show documentation
A Java library for reading Apache httpd configuration files
The newest version!
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2010-2014 Christian Bockermann
*
* This file is part of the jwall.org apache-config library. The apache-config library is
* a parsing library to handle Apache HTTPD configuration files.
*
* More information and documentation for the jwall-tools can be found at
*
* http://www.jwall.org/apache-config
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this
* program; if not, see .
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package org.jwall.apache.httpd.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import org.jwall.apache.httpd.service.DefaultFileSystem;
import org.jwall.apache.httpd.service.FileSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* This class reads a standard Apache configuration file and parses it into a
* tree of directives. As Apache supports inclusion of files, this parser will
* recursively parse included files as well.
*
*
* @author Christian Bockermann <[email protected]>
*
*/
public class ConfigParser {
/** A logger for this class */
static Logger log = LoggerFactory.getLogger(ConfigParser.class);
/** A reference to the file which this instances parses */
File file;
Stack pushback = new Stack();
/** The reader for reading lines */
BufferedReader reader;
/** The current line number */
int lineNumber = 1;
/** Number of bytes read ahead */
int readAhead = 0;
/** A buffer for creating multi line comments */
StringBuffer comment = new StringBuffer();
/** The root node of the tree which is created by this instance */
ApacheConfig root;
FileSystem fileSystem = new DefaultFileSystem();
// a global macro handler to store all macros, even if we recursively dig
// into included configuration files...
final static MacroHandler macroHandler = new MacroHandler();
File serverRoot = null;
public ConfigParser(Reader inputReader) throws IOException, ParseException {
this(inputReader, null);
}
public ConfigParser(Reader inputReader, File rootFile) throws IOException,
ParseException {
log.debug("Creating parser for direct reader...");
file = new File("");// new File( "/dev/null" );
reader = new BufferedReader(inputReader);
root = new ApacheConfig(file);
file = rootFile;
}
public ConfigParser(File configFile) throws IOException, ParseException {
this(configFile, new DefaultFileSystem());
}
/**
* This creates a new parser instance and sets up everything ready for
* parsing the file. The actual parsing is done by calling
* parseConfig()
.
*
* @param configFile
* @throws IOException
* @throws ParseException
*/
public ConfigParser(File configFile, FileSystem fs) throws IOException,
ParseException {
this.fileSystem = fs;
log.debug("Creating parser for '{}'", configFile.getCanonicalPath());
file = configFile;
reader = new BufferedReader(new InputStreamReader(
fileSystem.openInputStream(configFile))); // new FileReader(
// configFile ) );
root = new ApacheConfig(configFile);
String md5 = fs.md5(file);
root.setMd5sum(md5);
}
/**
*
*
* @param parent
* @return
* @throws IOException
* @throws ParseException
*/
public ApacheConfig parseConfig(ApacheConfig parent) throws IOException,
ParseException {
root = parent;
ApacheConfig cfg = parseConfig();
return cfg;
}
/**
*
* @return
* @throws IOException
* @throws ParseException
*/
public ApacheConfig parseConfig() throws IOException, ParseException {
if (file == null) {
log.debug("Parsing configuration directly from reader (no file provided)");
} else {
log.debug("parsing file {}", file);
}
String line = readLine();
while (line != null) {
//
// ok, we try to read blocks of commented and empty lines here
//
comment = new StringBuffer();
int start = lineNumber;
while (line != null && (isComment(line) || isEmpty(line))) {
log.debug("processing comment line: " + line);
if (comment.length() > 0 || !isEmpty(line))
comment.append(line.trim() + "\n");
line = readLine();
}
if (!comment.toString().trim().equals(""))
root.add(new Comment(comment.toString().replaceAll("<", "<")
.replaceAll(">", ">"), file, start, lineNumber));
//
// after we fully read a block of comments, the next line (!= null)
// is an config directive ;-)
//
if (line != null && !isEmpty(line) && !isComment(line)) {
//
// this must be a real config-line
//
if (line.trim().startsWith("<")) {
//
// we need to create a container directive that is a bit
// XML-like
//
if (line.trim().startsWith(" params = new ArrayList(
d.getArgs());
params.remove(0);
String expanded = macro.expand(params);
log.debug("Expanded macro:\n{}", expanded);
ConfigParser cp = new ConfigParser(
new StringReader(expanded), file);
ExpandedMacro cfg = new ExpandedMacro(
cp.parseConfig());
cfg.file = file;
cfg.location = new Position(file, lineNumber);
cfg.name = "ExpandedMacro";
cfg.setMacro(macro.getName());
cfg.getParameters().addAll(params);
cfg.getArgs().addAll(d.getArgs());
// for (AbstractDirective ad : cfg.getChildren()) {
// root.add(ad);
// }
root.add(cfg);
} else {
root.add(d);
}
}
}
}
line = readLine();
}
return root;
}
public void includeFile(ApacheConfig parent, String line)
throws IOException, ParseException {
log.debug("Processing file inclusion: '{}', current file is: {}",
line, file);
File base = null;
if (file != null) {
base = file.getParentFile();
}
if (this.serverRoot != null) {
log.debug("'ServerRoot' setting found in local context, using this for relative file resolution...");
base = serverRoot;
}
log.debug(
" default base directory for resolving relative paths is '{}'",
base);
//
// line should be something like "\\s+Include\\s+/path/to..." here
//
String[] token = line.trim().split("\\s+");
String fileName = token[1].trim();
while (fileName.trim().startsWith("\""))
fileName = fileName.trim().substring(1);
while (fileName.trim().endsWith("\""))
fileName = fileName.trim().substring(0,
fileName.trim().length() - 1);
File includeFile = new File(fileName);
// this is the include instance which we add nested elements to
//
Include includeDirective = new Include(line, this.file, this.lineNumber);
//
// now we handle the include pattern and create the nested apache config
// tree which needs to be added to the include instance.
//
if (!fileName.startsWith(File.separator)) {
log.debug("Including file " + fileName);
log.debug(" relative to base=" + base);
includeFile = new File(base + File.separator + fileName);
}
if (includeFile.getName().indexOf("*") >= 0
|| includeFile.getName().endsWith("/")) {
log.debug("Including all files matching "
+ includeFile.getAbsolutePath());
String pattern = "";
if (includeFile.getName().endsWith("/")) {
// we include all files within a directory
// by simply creating an appopriate pattern
pattern = includeFile.getName() + ".*";
} else {
// filename is a wildcard!
// we need to include all matching files
pattern = includeFile.getName().replaceAll("\\*", ".*");
}
File dir = includeFile.getParentFile();
if (dir != null) {
log.debug("Listing patterns from " + dir);
if (!fileSystem.exists(dir))
throw new ParseException("Syntax error on line "
+ this.lineNumber + " of file "
+ this.file.getAbsolutePath()
+ ": Include directory \"" + dir.getAbsolutePath()
+ "\" not found");
for (File fl : fileSystem.listFiles(dir)) {
log.debug("Checking include file: {}", fl);
String f = fl.getName();
if (!".".equals(f) && !"..".equals(f) && f.matches(pattern)) {
log.debug("Including file due to pattern match: file="
+ f);
// System.err.println("Including file due to pattern match: file="+f);
File includedFile = new File(dir.getAbsolutePath()
+ File.separator + f);
String md5 = fileSystem.md5(includedFile);
ConfigParser parser = new ConfigParser(includedFile,
this.fileSystem);
ApacheConfig nestedConfig = parser.parseConfig();
nestedConfig.setMd5sum(md5);
nestedConfig.setFile(includedFile);
nestedConfig.setIncludedBy(this.file.getAbsolutePath()
+ ":" + this.lineNumber);
includeDirective.add(nestedConfig);
// root.add( nestedConfig );
}
}
if (!includeDirective.getChildren().isEmpty())
root.add(includeDirective);
} else
log.warn("parent file of " + includeFile.getAbsolutePath()
+ " is null!");
} else {
//
// here we completely resolved the file-pattern to a real file
// that needs to be parsed using a new parser
//
if (fileSystem.isDirectory(includeFile)) {
log.debug("Including a directory: {}",
includeFile.getAbsolutePath());
for (File include : fileSystem.listFiles(includeFile)) {
if (!fileSystem.isDirectory(include)) {
log.debug("\tincluding {}", include.getAbsolutePath());
ConfigParser parser = new ConfigParser(include,
this.fileSystem);
ApacheConfig d = parser.parseConfig();
d.setFile(include);
d.setIncludedBy(this.file.getAbsolutePath() + ":"
+ this.lineNumber);
log.debug("{}", d.toPlainTxt());
includeDirective.add(d);
root.add(includeDirective);
} else
log.debug("Not including directory {}", include);
}
} else {
if (fileSystem.exists(includeFile)
&& fileSystem.canRead(includeFile)) {
ConfigParser parser = new ConfigParser(includeFile,
this.fileSystem);
ApacheConfig d = parser.parseConfig();
d.setFile(includeFile);
d.setIncludedBy(this.file.getAbsolutePath() + ":"
+ this.lineNumber);
// the resulting sub-config-tree is nested into the parent
//
includeDirective.add(d);
root.add(includeDirective);
}
}
}
// root.add( includeDirective );
}
/**
*
* @param line
* @return
* @throws IOException
* @throws ParseException
*/
public AbstractDirective parseDirective(String line) throws IOException,
ParseException {
if (line.trim().matches("^#\\w.*")) {
log.trace("Found disabled directive: " + line);
}
if (line.trim().startsWith("<") && !line.trim().startsWith("")) {
if (macroHandler.isMacroStart(line)) {
log.debug("handling macro snippet...");
Macro macro = macroHandler.readMacro(line, this);
return macro;
}
log.debug("parsing container: " + line);
AbstractDirective d = parseContainerDirective(line);
return d;
} else {
log.debug("parsing line-directive: " + line);
AbstractDirective d = createDirective(line);
if (d != null) {
if ("serverroot".equalsIgnoreCase(d.getName())) {
this.serverRoot = new File(d.getArgs().get(0));
log.debug(
"Found 'ServerRoot' setting, changing parser path for relative file inclusions to {}",
serverRoot);
}
return d;
}
boolean enabled = true;
if (line.trim().matches("^#\\w.*")) {
line = line.trim().substring(1);
enabled = false;
}
String[] token = line.trim().split(" ");
String[] args = new String[token.length - 1];
for (int i = 1; i < token.length; i++)
args[i - 1] = token[i];
d = new LineDirective(line, file, lineNumber);
d.setEnabled(enabled);
return d;
}
}
/**
*
* This method reads a xml-style directive that may contain other
* configuration options as well as more nested xml-style containers
*
* @param line
* @return
* @throws IOException
* @throws ParseException
*/
public ContainerDirective parseContainerDirective(String line)
throws IOException, ParseException {
//
// create the container
//
ContainerDirective parent = createContainer(line);
String closingTag = "" + parent.getName() + ">"; // this becomes the
// end-tag
boolean closed = false;
log.debug("Found container directive: " + line);
log.debug(" looking for closing tag: " + closingTag);
String l = null;
do {
l = readLine();
if (l != null && l.trim().startsWith(closingTag))
closed = true;
if (l == null && !closed)
throw new ParseException("File \"" + file.getAbsolutePath()
+ "\" ended before directive " + parent.getName()
+ " has been closed!\n" + " Start of directive "
+ parent.getName() + " is at line "
+ parent.getLocation().getLine());
StringBuffer comment = new StringBuffer();
int start = lineNumber;
while (l != null && (isComment(l) || isEmpty(l))) {
log.debug(" processing comment line: " + l);
comment.append(l + "\n");
l = readLine();
}
if (!comment.toString().trim().equals(""))
parent.add(new Comment(comment.toString()
.replaceAll("<", "<").replaceAll(">", ">"), file,
start, lineNumber));
/*
* if( l != null && l.trim().equals( closingTag ) ){ closed = true;
* log.info( "Found closing tag: " + l ); return parent; }
*/
if (l != null && !l.trim().startsWith(closingTag)) {
log.debug("Handling nested line " + l);
if (l.trim().startsWith("Include")) {
includeFile(root, l);
} else {
AbstractDirective ch = parseDirective(l);
if (ch != null) {
log.debug(" -> Adding directive " + ch.getName()
+ ", class = " + ch.getClass().getName());
parent.add(ch);
}
}
} else {
log.debug("Found closing tag: " + l);
if (l == null && !closed)
throw new ParseException("File \"" + file.getAbsolutePath()
+ "\" ended before directive \""
+ parent.getRawLine() + "\" has been closed!\n"
+ " Start of directive " + parent.getName()
+ " is at line " + parent.getLocation().getLine());
return parent;
}
} while (l != null && !l.trim().startsWith(closingTag));
return parent;
}
public void unread(String line) {
pushback.push(line);
}
/**
* This method will read a line from the underlying reader. In case of a
* multi-line, all continuing lines are read and merged to return a final
* version of all concatenated multi-line parts.
*
* @return
* @throws IOException
*/
public String readLine() throws IOException {
if (!pushback.isEmpty()) {
return pushback.pop();
}
String line = reader.readLine();
lineNumber += readAhead;
readAhead = 1;
//
// if the current line does not end with '\', we are finished reading
//
if (!isMultiline(line))
return line;
//
// otherwise we have to build the line by appending all lines until a
// non-multiline or EOF is reached
//
// this buffer stores the line we finally return
StringBuffer finalLine = new StringBuffer(removeMultilineChar(line));
do {
//
// append the next line to the buffer until we reach a non-multiline
//
line = reader.readLine();
lineNumber++;
finalLine.append(removeMultilineChar(line));
} while (line != null && isMultiline(line));
return finalLine.toString();
}
public boolean isMultiline(String line) {
return line != null && line.trim().endsWith("\\");
}
public String removeMultilineChar(String line) {
int idx = line.lastIndexOf("\\");
if (idx >= 0)
return line.substring(0, idx);
return line;
}
/**
* This method simply checks if the given line line
is empty,
* i.e. contains all spaces.
*
* @param line
* The line to check.
* @return true, if there is at least one word-character in the line.
*/
public boolean isEmpty(String line) {
return (line == null || line.trim().equals(""));
}
/**
* This method is used for defining comment-lines. Comment lines start with
* a '#' character, possibly preceded by one or more spaces/tabs.
*
* @param line
* The line to check.
* @return true, if the line starts with a '#' as the first non-space
* character.
*/
public boolean isComment(String line) {
if (line.trim().startsWith("#") || line.trim().equals("#"))
return true;
return false;
}
/**
*
* This is a kind of factory-function. It is called whenever a container
* directive (xml-style directive) is found. Later on, other parsers might
* register themselves to the ConfigParser class to be called as "their"
* directive has been hit.
*
* @param line
* The line holding the start-tag of a xml-style container
* directive.
* @return An instance of the appopriate container directive.
* @throws ParseException
*
*/
public ContainerDirective createContainer(String line)
throws ParseException {
log.debug("Creating container-directive from start-line: " + line);
line = line.trim();
int end = 1; // the first char is '<'
while (end < line.length() && line.charAt(end) != ' ')
end++;
String name = line.substring(1, end).toLowerCase();
if (name.equals("directory"))
return new Directory(line, file, lineNumber);
if (name.equals("ifmodule"))
return new IfModule(line, file, lineNumber);
if (name.equals("virtualhost"))
return new VirtualHost(line, file, lineNumber);
if (name.equals("proxy"))
return new Proxy(line, file, lineNumber);
if (name.equals("filesmatch") || name.equals("files")
|| name.equals("directorymatch")
|| name.equals("locationmatch"))
return new FilesMatch(line, file, lineNumber);
if (name.equals("macro")) {
return new Macro(line, file, lineNumber);
}
return new ContainerDirective(line, file, lineNumber);
}
/**
*
* As with the container directives this method creates a directive
* (one-liner) from the given line. Similar to the
* createContainer
-method, this may provide modular parser
* objects in the future.
*
* @param line
* The line holding the directive.
* @return An instance of the appopriate directive class.
* @throws ParseException
*/
public AbstractDirective createDirective(String line) throws ParseException {
log.debug("Creating simple line-directive from: " + line);
String param = line.trim();
String[] args = param.split(" ");
String name = args[0];
if (name.equals("DocumentRoot"))
return new DocumentRoot(line, file, lineNumber);
if (name.equals("SecRule")) {
log.trace("Creating SecRule directive[" + file + ":" + lineNumber
+ "] >" + line + "<");
return new SecRule(line, file, lineNumber);
}
if (name.equals("SecAction"))
return new SecAction(line, file, lineNumber);
if (name.equals("RewriteRule"))
return new RewriteRule(line, file, lineNumber);
if (name.equals("SecArgumentSeparator"))
return new SecArgumentSeparator(line, file, lineNumber);
log.trace("Creating generic directive[" + file + ":" + lineNumber
+ "] >" + line + "<");
return new LineDirective(line, file, lineNumber);
}
/**
* @param args
*/
public static void main(String[] args) {
try {
String configTxt = "SecRule \"REQUEST_METHOD\" \"@rx ^(?:GET|HEAD)$\" \"phase:2,chain,t:none,deny,log,auditlog,status:400,msg:'GET or HEAD requests with bodies',severity:2,id:960011,tag:PROTOCOL_VIOLATION/EVASION\"\n"
+ "SecRule \"&REQUEST_HEADERS:Accept\" \"@eq 0\" \"phase:2,chain,skip:1,t:none,log,auditlog,msg:'Request Missing an Accept Header',severity:2,id:960015,tag:PROTOCOL_VIOLATION/MISSING_HEADER\"\n"
+ "SecRule \"REQUEST_METHOD\" \"!@rx ^OPTIONS$\" \"phase:2,log,auditlog,pass,t:none\"\n"
+ "SecRule \"&REQUEST_HEADERS:Content-Type\" \"@eq 0\" \"phase:2,pass,chain,t:none,log,auditlog,msg:'Request Containing Content, but Missing Content-Type header',id:960904,severity:4\"\n"
+ "SecAction \"phase:2,auditlog,nolog,skipAfter:959009\"\n"
+ "SecAction \"phase:2,auditlog,nolog,skipAfter:959007\"\n"
+ "SecAction \"phase:2,auditlog,nolog,skipAfter:959904\"\n"
+ "SecAction \"phase:2,auditlog,nolog,id:999501,skipAfter:959001\"\n"
+ "SecAction \"phase:2,auditlog,nolog,skipAfter:959906\"\n"
+ "SecRule \"REQUEST_FILENAME|ARGS|ARGS_NAMES|REQUEST_HEADERS|XML:/*|!REQUEST_HEADERS:Referer\" \"@pm jscript onsubmit copyparentfolder javascript meta onmove onkeydown onchange onkeyup activexobject expression onmouseup ecmascript onmouseover vbscript: \n");
for (AbstractDirective d : config.getChildren()) {
if (d.getName().startsWith("Sec"))
System.out.println(d.toXML());
}
// System.out.println( config.toXML() );
System.out.println("");
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
/*
* try { File cfg = new File( "/www/apache2/conf/httpd.conf" );
*
* if( args.length > 0 ){ cfg = new File( args[0] ); }
*
* ConfigParser parser = new ConfigParser( cfg ); ApacheConfig config =
* parser.parseConfig(); log.debug("config:\n"+config.toXML() ); } catch
* (Exception e){ e.printStackTrace(); }
*/
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy