All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jwall.apache.httpd.config.ConfigParser Maven / Gradle / Ivy

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(""; // 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