(wadm VI) Walking with the unknown - parsing Apache httpd.conf
I have already shown you how to implement a ACL parser using wadm tcl. In this section, I will try to take you along as I try to implement a simple parser for Apache httpd.conf and use it to implement a similar configuration in Sun Java System WebServer.
Looking at the apache httpd.conf
The httpd.conf looks like this:
httpd.conf
# This is the main Apache HTTP server configuration file. It contains the
# configuration directives that give the server its instructions.
ServerRoot "/usr/local/apache2"
Listen 800
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule mime_module modules/mod_mime.so
..snipped..
LoadModule rewrite_module modules/mod_rewrite.so
..snipped..
ServerAdmin me@sun.com
ServerName www.example.com:80
DocumentRoot "/usr/local/www"
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
ScriptAlias /codestriker/ /space/codestriker/codestriker-1.9.2-alpha-5/cgi-bin/
Alias /codestrikerhtml/ /space/codestriker/codestriker-1.9.2-alpha-5/html/
<IfModule dir_module>
DirectoryIndex index.html
</IfModule>
<FilesMatch "\^\\.ht">
Order allow,deny
Deny from all
</FilesMatch>
ErrorLog logs/error_log
LogLevel warn
<IfModule log_config_module>
LogFormat "%h %l %u %t \\"%r\\" %>s %b \\"%{Referer}i\\" \\"%{User-Agent}i\\"" combined
LogFormat "%h %l %u %t \\"%r\\" %>s %b" common
<IfModule logio_module>
LogFormat "%h %l %u %t \\"%r\\" %>s %b \\"%{Referer}i\\" \\"%{User-Agent}i\\" %I %O" combinedio
</IfModule>
CustomLog logs/access_log common
</IfModule>
DefaultType text/plain
<IfModule mime_module>
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
AddOutputFilter INCLUDES .shtml
</IfModule>
MIMEMagicFile conf/magic
The files look like tcl except for the
Since we are looking at a slightly more complex parsing than the acls, we will take a similar but slightly different route.
The Parsing.
First let us see if we can capture the pattern that we noticed above. here is our temporary conf file
temp.conf
ServerName agneyam
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
Here is the parser that we want to run on it.
apache.tcl
namespace eval Apache {
proc parse {file} {
set text [read_file $file]
eval $text
}
proc read_file {file} {
set f [open $file r]
set res [read -nonewline $f]
close $f
return $res
}
}
As you can see, We are just trying to apply the strategy previously described in ACL parsing.
> source apache.tcl
> Apache::parse temp.conf
invalid command name "ServerName"
Ok so we implement the ServerName
proc ServerName args {
}
> source apache.tcl
> Apache::parse temp.conf
invalid command name "<Directory"
While it is easy for us to define two more commands <Directory and </Directory, it seems that there should be a better way, What we can do is ask the tcl parser to call us when it sees things of this sort. Tcl provides a convenient way for us to do that .
The Unknown.
Tcl calls a procedure called unknown when ever it finds a command or procedure
that was not previously defined. We will take advantage of that to get the
What we will do is to redefine the unknown and make it into our own procedure.
apache.tcl
namespace eval Apache {
variable stack
proc parse {file} {
set text [read_file $file]
init_parse
if [catch {eval $text} err] {
puts "Eval:$err"
}
exit_parse
}
proc read_file {file} {
set f [open $file r]
set res [read -nonewline $f]
close $f
return $res
}
proc init_parse {} {
rename ::unknown _unknown
proc ::unknown args {
Apache::invoke $args
}
set Apache::stack {}
}
proc exit_parse {} {
rename ::unknown ""
rename _unknown ::unknown
}
proc invoke arg {
puts ": $arg"
}
}
Trying it
> source apache.tcl
> Apache::parse temp.conf
: ServerName agneyam
: <Directory />
: Options FollowSymLinks
: AllowOverride All
: </Directory>
As you can see, We redefined and saved the current unknown to Apache::_unknown before parsing, evaluated the file and after that changed the Apache::_unknown to the global unknown.
Inside our parser, we redefined the unknown to call the Apache::invoke instead. so that we get to interpret the procedures instead of tcl evaluator.
Now, we need to handle these tags better.
Handling the tags
Our aim is to just define procedures for all the normal directives (Those that are *not* of the
form
apache.tcl
namespace eval Apache {
variable stack
variable not_impl
set not_impl {}
proc parse {file} {
set text [read_file $file]
init_parse
if [catch {eval $text} err] {
puts "Eval:$err"
}
exit_parse
}
proc show {} {
puts "Not Implemented:"
puts "________________"
foreach {i} $Apache::not_impl {
puts $i
}
}
proc read_file {file} {
set f [open $file r]
set res [read -nonewline $f]
close $f
return $res
}
proc init_parse {} {
rename ::unknown _unknown
proc ::unknown args {
Apache::invoke $args
}
set Apache::stack {}
}
proc exit_parse {} {
rename ::unknown ""
rename _unknown ::unknown
}
proc push_stack arg {
set Apache::stack [concat $Apache::stack $arg]
}
proc pop_stack {} {
set popd [lindex $Apache::stack end]
set Apache::stack [lrange $Apache::stack 0 end-1]
return $popd
}
proc invoke arg {
set word [lindex $arg 0]
switch -regexp $word {
{\^ \*</.\*} {
if {[regexp {\^ \*< \*/([\^ ]+) \*> \*$} $arg all one]} {
set p [pop_stack]
invoke_proc $p -exit
}
return
}
{\^ \*<.\*} {
if {[regexp {\^ \*<([\^ ]+) \*(.\*)> \*$} $arg all one rest]} {
push_stack $one
invoke_proc $one -init $rest
}
return
}
default {
#the only directives that come here must be those that are not done.
notimpl $word
}
}
}
proc invoke_proc {p args} {
if [llength [info proc $p]] {
$p $args
} else {
notimpl $p
}
}
proc notimpl arg {
set Apache::not_impl [concat $Apache::not_impl $arg]
}
}
As you can see, when we find a tag of sort
Using it
> source apache.tcl
> Apache::parse temp.conf
> Apache::show
Not Implemented:
________________
ServerName
Directory
Options
AllowOverride
Let us add something more to the apache httpd.conf
temp.conf
ServerName agneyam
<IfModule log_config_module>
LogFormat "%h %l %u %t \\"%r\\" %>s %b \\"%{Referer}i\\" \\"%{User-Agent}i\\"" combined
LogFormat "%h %l %u %t \\"%r\\" %>s %b" common
<IfModule logio_module>
LogFormat "%h %l %u %t \\"%r\\" %>s %b \\"%{Referer}i\\" \\"%{User-Agent}i\\" %I %O" combinedio
</IfModule>
CustomLog logs/access_log common
</IfModule>
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
Implementing the IfModule
There are two ways we can handle the IfModule,
- Do it statically, That is evaluate the IfModule with what is available in the current LoadedModules array (Assume such an array is constructed and is available.)
- Do it dynamically, Instead of Evaluating the IfModule during parsing state, add the condition to the script that will get evaluated when the script is run.
We will go for the second option since that is more powerful.
apache.tcl
namespace eval Apache {
variable stack
variable not_impl
#commands to be evaluated
variable text
proc parse {file} {
set text [read_file $file]
regsub -all {\\">} $text {\\" >} text
pre_conf
init_parse
if [catch {eval $text} err] {
puts "Eval:$err"
}
exit_parse
}
proc pre_conf {} {
set Apache::stack {}
set Apache::not_impl {}
set Apache::text {}
array set Apache::info {}
array set Apache::module {}
}
proc show {} {
puts "Not Implemented:"
puts "________________"
foreach {i} [lsort [uniq $Apache::not_impl]] { puts $i }
puts "________________"
foreach {i} $Apache::text { puts $i }
}
proc uniq {l} {
if { $l == {} } { return {} }
array set arr {}
foreach element $l { set arr($element) "" }
set result [array names arr]
return $result
}
proc read_file {file} {
set f [open $file r]
set res [read -nonewline $f]
close $f
return $res
}
proc | args {
lappend Apache::text [join $arg " "]
}
proc init_parse {} {
rename ::unknown _unknown
proc ::unknown args {
Apache::invoke $args
}
}
proc exit_parse {} {
rename ::unknown ""
rename _unknown ::unknown
}
proc push_stack arg {
set Apache::stack [concat $Apache::stack $arg]
}
proc pop_stack {} {
set popd [lindex $Apache::stack end]
set Apache::stack [lrange $Apache::stack 0 end-1]
return $popd
}
proc invoke arg {
set word [lindex $arg 0]
switch -regexp $word {
{\^ \*</.\*} {
if {[regexp {\^ \*< \*/([\^ ]+) \*> \*$} $arg all one]} {
set p [pop_stack]
invoke_proc $p -exit
}
return
}
{\^ \*<.\*} {
if {[regexp {\^ \*<([\^ ]+) \*(.\*)> \*$} $arg all one rest]} {
push_stack $one
invoke_proc $one -init $rest
}
return
}
default {
#the only directives that come here must be those that are not done.
notimpl $word $arg
}
}
}
proc invoke_proc {p args} {
if [llength [info proc $p]] {
$p $args
} else {
notimpl $p $args
}
}
proc notimpl {p arg} {
#Uncomment the next line to get the not implenented procedures
#in their context.
| "#NI ($p) $arg"
set Apache::not_impl [concat $Apache::not_impl $p]
}
#==================================================
# The Engine
#==================================================
proc IfModule args {
set arg [lindex $args 0]
switch -- [lindex $arg 0] {
{-init} {
set cur [lindex $arg 1]
#check if it is of type !xxxx , if it is take the reverse.
if {[regexp {\^ \*!([\^ ]+)$} $cur all one] } {
| "if {!\\[info exist Apache::module($one)\\]} {" ;#}
} else {
| "if \\[info exist Apache::module($cur)\\] {" ;#}
}
}
{-exit} {
#;{
| "}"
}
}
}
}
Using it,
> source apache.tcl
> Apache::parse temp.conf
> Apache::show
Not Implemented:
________________
AllowOverride
CustomLog
Directory
LogFormat
Options
ServerName
________________
#NI (ServerName) ServerName agneyam
if [info exist Apache::module(log_config_module)] {
#NI (LogFormat) LogFormat {%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"} combined
#NI (LogFormat) LogFormat {%h %l %u %t "%r" %>s %b} common
if [info exist Apache::module(logio_module)] {
#NI (LogFormat) LogFormat {%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i" %I %O} combinedio
}
#NI (CustomLog) CustomLog logs/access_log common
}
#NI (Directory) -init /
#NI (Options) Options FollowSymLinks
#NI (AllowOverride) AllowOverride All
#NI (Directory) -exit
As you can see, the IfModule adds if condition to the script produced at the end. Now let us turn our attension to the engine.
The Engine
The Engine is very simple. You have to define a procedure for each directive you want to translate. You will be given the arguments via the $args list. After figuring out what commands to call, just call them using the ‘|’ procedure. (as demonstrated in the IfModule.)
Handling the tags are also simple as shown in the IfModule, each such procedure (say Directory) will take a list whose first element will be ‘-init’ when it enters the tag, and ‘-exit’ when it exits the tag. You will also get the rest of arguments to tag entry along with -init.
(Look for the comment with #Engine to see the engine starting.)
apache.tcl
namespace eval Apache {
variable stack
variable not_impl
variable info
variable module
#commands to be evaluated
variable text
proc parse {file} {
set text [read_file $file]
#massage it gently.
regsub -all {\\">} $text {\\" >} text
pre_conf
init_parse
if [catch {eval $text} err] {
puts "Eval:$err"
}
exit_parse
}
proc pre_conf {} {
set Apache::stack {}
set Apache::not_impl {}
set Apache::text {}
array set Apache::info {}
array set Apache::module {}
#--------------------------------
set Apache::info(config) apache
set Apache::info(config_port) 10000
set Apache::info(server_name) apache_mig
set Apache::info(default_vs) $Apache::info(config)
| create-config --http-port $Apache::info(config_port) --server-name $Apache::info(server_name) $Apache::info(config)
}
proc show {} {
puts "Not Implemented:"
puts "________________"
foreach {i} [lsort [uniq $Apache::not_impl]] { puts $i }
puts "________________"
foreach {i} $Apache::text { puts $i }
}
proc uniq {l} {
if { $l == {} } { return {} }
array set arr {}
foreach element $l { set arr($element) "" }
set result [array names arr]
return $result
}
proc read_file {file} {
set f [open $file r]
set res [read -nonewline $f]
close $f
return $res
}
proc | args {
lappend Apache::text [join $args " "]
}
proc init_parse {} {
rename ::unknown _unknown
proc ::unknown args {
Apache::invoke $args
}
}
proc exit_parse {} {
rename ::unknown ""
rename _unknown ::unknown
}
proc push_stack arg {
set Apache::stack [concat $Apache::stack $arg]
}
proc pop_stack {} {
set popd [lindex $Apache::stack end]
set Apache::stack [lrange $Apache::stack 0 end-1]
return $popd
}
proc invoke arg {
set word [lindex $arg 0]
switch -regexp $word {
{\^ \*</.\*} {
if {[regexp {\^ \*< \*/([\^ ]+) \*> \*$} $arg all one]} {
set p [pop_stack]
invoke_proc $p -exit
}
return
}
{\^ \*<.\*} {
if {[regexp {\^ \*<([\^ ]+) \*(.\*)> \*$} $arg all one rest]} {
push_stack $one
invoke_proc $one -init $rest
}
return
}
default {
#the only directives that come here must be those that are not done.
notimpl $word $arg
}
}
}
proc invoke_proc {p args} {
if [llength [info proc $p]] {
$p $args
} else {
notimpl $p $args
}
}
proc notimpl {p arg} {
#Uncomment the next line to get the not implenented procedures
#in their context.
#| "#NI ($p) $arg"
set Apache::not_impl [concat $Apache::not_impl $p]
}
#==================================================
# The Engine
#==================================================
proc IfModule args {
set arg [lindex $args 0]
switch -- [lindex $arg 0] {
{-init} {
set cur [lindex $arg 1]
#check if it is of type !xxxx , if it is take the reverse.
if {[regexp {\^ \*!([\^ ]+)$} $cur all one] } {
| "if {!\\[info exist Apache::module($one)\\]} {" ;#}
} else {
| "if \\[info exist Apache::module($cur)\\] {" ;#}
}
}
{-exit} {
#;{
| "}"
}
}
}
proc ServerRoot args {
set Apache::info(server_root) $args
}
proc ServerAdmin args {
set Apache::info(server_admin) [lindex $args 0]
}
proc ServerName args {
set Apache::info(server_name) [lindex $args 0]
}
proc DocumentRoot args {
set droot [lindex $args 0]
| set-virtual-server-prop --config $Apache::info(config) --vs $Apache::info(default_vs) \\
document-root=$droot
}
proc LogLevel args {
set arg [lindex $args 0]
set log warning
#emerg System is unstable
#alert Immediate action required
#crit Critical error
#error Non-critical error
#warn Warning
#notice Normal but significant
#info Informational
#debug Debug level
#finest
#finer
#fine
#info
#warning
#failure
#config
#security
#catastrophe
switch $arg -- {
emerg {
set log catastrophe
}
alert {
set log failure
}
crit {
set log failure
}
errof {
set log warning
}
warn {
set log warning
}
notice {
set log info
}
info {
set log fine
}
debug {
set log finest
}
}
| set-log-prop --config $Apache::info(config) log-level=$log
}
proc ErrorLog args {
set log [lindex $args 0]
| set-log-prop --config $Apache::info(config) log-file=$log
}
proc AccessFileName args {
set af [lindex $args 0]
| enable-htaccess --config $Apache::info(config) --vs $Apache::info(default_vs) \\
--config-file $af
}
proc AddType args {
set type [lindex $args 0]
set ext [join [lrange $args 1 end] ","]
regsub -all {\\.} $ext {} ext
| create-mime-type --config $Apache::info(config) --extensions $ext $type
}
proc DefaultType args {
#this does not have an equivalent in sjswebsrever.
}
proc Alias args {
set alias [lindex $args 0]
set fs [lindex $args 1]
| create-document-dir --config $Apache::info(config) --uri-prefix $alias \\
--directory $fs --vs $Apache::info(default_vs)
}
proc CgiAlias args {
set alias [lindex $args 0]
set fs [lindex $args 1]
| create-cgi-dir --config $Apache::info(config) --uri-prefix $alias \\
--directory $fs --vs $Apache::info(default_vs)
}
proc TypesConfig args {
set mf [lindex $args 0]
| set-config-prop --config $Apache::info(config) mime-file=$mf
}
proc LoadModule args {
| "set Apache::module([lindex $args 0]) [lrange $args 1 end]"
}
proc Listen args {
#listen could come in two forms:
#8000
#192.168.1.1:8000
set port 0
set ip {}
set arg [lindex $args 0]
switch -regexp -- $arg {
{:} {
regexp {(.+):(.+)} $arg all ip port
}
default {
set port $arg
}
}
#The listen could happen multiple times. when it does, we need to change the http-listener name
set lst http-listener-1
if [info exists Apache::info(http-listener-cur)] {
set lst "$Apache::info(http-listener-cur)-1"
set ipprop ""
if [string length $ip] {
set ipprop "--ip $ip"
}
eval "| create-http-listener --config $Apache::info(config) \\
--listener-port $port $ipprop \\
--server-name $Apache::info(server_name) \\
--default-virtual-server-name $Apache::info(default_vs) \\
$ipprop $lst"
set Apache::info(http-listener-cur) $lst
} else {
set ipprop ""
if [string length $ip] {
set ipprop "ip=$ip"
}
eval "| set-http-listener-prop --config $Apache::info(config) \\
--http-listener $lst port=$port $ipprop"
}
}
}
Using It
> source apache.tcl
> Apache::parse httpd.conf
> Apache::show
Not Implemented:
________________
Allow
AllowOverride
CustomLog
Deny
Directory
DirectoryIndex
FilesMatch
Group
LogFormat
Options
Order
SSLRandomSeed
ScriptAlias
SetHandler
User
________________
create-config --http-port 10000 --server-name apache_mig apache
set-http-listener-prop --config apache --http-listener http-listener-1 port=800
if {![info exist Apache::module(mpm_winnt_module)]} {
if {![info exist Apache::module(mpm_netware_module)]} {
}
}
set-virtual-server-prop --config apache --vs apache document-root=/usr/local/www
create-document-dir --config apache --uri-prefix /codestrikerhtml/ --directory /space/codestriker/codestriker-1.9.2-alpha-5/html/ --vs apache
create-document-dir --config apache --uri-prefix /lxr_scratch --directory /usr/local/lxr_scratch --vs apache
create-document-dir --config apache --uri-prefix /lxr_40rtm --directory /usr/local/lxr_40rtm --vs apache
create-document-dir --config apache --uri-prefix /lxr_36rtm --directory /usr/local/lxr_36rtm --vs apache
create-document-dir --config apache --uri-prefix /lxr_nspr --directory /usr/local/lxr_nspr --vs apache
create-document-dir --config apache --uri-prefix /lxr_nss --directory /usr/local/lxr_nss --vs apache
create-document-dir --config apache --uri-prefix /lxr_squid --directory /usr/local/lxr_squid --vs apache
create-document-dir --config apache --uri-prefix /lxr_ws70 --directory /usr/local/lxr_ws70 --vs apache
if [info exist Apache::module(dir_module)] {
}
enable-htaccess --config apache --vs apache --config-file .htaccess
set-log-prop --config apache log-file=logs/error_log
set-log-prop --config apache log-level=warning
if [info exist Apache::module(log_config_module)] {
if [info exist Apache::module(logio_module)] {
}
}
if [info exist Apache::module(alias_module)] {
}
if [info exist Apache::module(cgid_module)] {
}
if [info exist Apache::module(mime_module)] {
set-config-prop --config apache mime-file=conf/mime.types
create-mime-type --config apache --extensions Z application/x-compress
create-mime-type --config apache --extensions gz,tgz application/x-gzip
}
if [info exist Apache::module(ssl_module)] {
}
The current apache.tcl is available here