Assorted goodies
[hcoop/zz_old/domtool.git] / src / apache / apache.sml
CommitLineData
182a2654
AC
1(*
2Domtool (http://hcoop.sf.net/)
3Copyright (C) 2004 Adam Chlipala
4
5This program is free software; you can redistribute it and/or
6modify it under the terms of the GNU General Public License
7as published by the Free Software Foundation; either version 2
8of the License, or (at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program; if not, write to the Free Software
17Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18*)
19
20(* Apache vhost management module, with Webalizer support *)
21
22structure Apache :> APACHE =
23struct
24 open Config ApacheConfig Util
25
26 val vhosts = ref (NONE : TextIO.outstream option)
1e2e348e 27 val loggroups = ref (NONE : TextIO.outstream option)
182a2654 28
1e2e348e
AC
29 fun init () = (vhosts := SOME (TextIO.openOut (scratchDir ^ "/vhosts.conf"));
30 loggroups := SOME (TextIO.openOut (scratchDir ^ "/loggroups")))
182a2654 31 fun finish () = (TextIO.closeOut (valOf (!vhosts));
1e2e348e
AC
32 vhosts := NONE;
33 TextIO.closeOut (valOf (!loggroups));
34 loggroups := NONE)
182a2654 35
05060d16
AC
36 val noargs = ["redirect", "R", "forbidden", "F", "gone", "G", "last", "L", "chain", "C", "nosubeq", "NS", "nocase", "NC", "qsappend", "QSA", "noescape", "NE", "passthrough", "PT"]
37
38 fun checkRewriteArgs (path, args) =
39 if size args < 2 orelse String.sub (args, 0) <> #"[" orelse String.sub (args, size args - 1) <> #"]" then
40 (Domtool.error (path, "Not in brackets: " ^ args);
41 false)
42 else let
43 val args = String.substring (args, 1, size args - 2)
44 val fields = String.fields (fn ch => ch = #",") args
45
46 fun checkField f =
47 case String.fields (fn ch => ch = #"=") f of
48 [flag] => List.exists (fn x => x = flag) noargs orelse (Domtool.error (path, "Unknown argument-free flag " ^ flag);
49 false)
05060d16
AC
50 | ["type", _] => true
51 | ["T", _] => true
52 | ["skip", num] => isNat num orelse (Domtool.error (path, "Bad skip number " ^ num); false)
53 | ["S", num] => isNat num orelse (Domtool.error (path, "Bad skip number " ^ num); false)
54 | ["env", varval] =>
55 (case String.fields (fn ch => ch = #":") varval of
56 [_, _] => true
57 | _ => (Domtool.error (path, "Bad env setting " ^ varval);
58 false))
59 | ["E", varval] =>
60 (case String.fields (fn ch => ch = #":") varval of
61 [_, _] => true
62 | _ => (Domtool.error (path, "Bad env setting " ^ varval);
63 false))
64 | _ => (Domtool.error (path, "Unknown or disallowed mod_rewrite flag " ^ f);
65 false)
66 in
67 List.all checkField fields
68 end
69
182a2654
AC
70 fun handler {path, domain, parent, vars, paths, users, groups} =
71 let
72 val _ = Domtool.dprint ("Reading host " ^ path ^ " for " ^ domain ^ "....")
73
74 val vhosts = valOf (!vhosts)
1e2e348e 75 val loggroups = valOf (!loggroups)
182a2654
AC
76
77 val hf = TextIO.openIn path
78 val rewrite = ref false
79
80 val conf = TextIO.openOut (wblConfDir ^ "/" ^ domain ^ ".conf")
81 val _ = TextIO.output (conf, "LogFile\t" ^ logDir ^ domain ^ "-access.log\n" ^
82 "OutputDir\t" ^ wblDocDir ^ "/" ^ domain ^ "\n" ^
83 "HostName\t" ^ domain ^ "\n" ^
84 "HideSite\t" ^ domain ^ "\n" ^
85 "HideReferrer\t" ^ domain ^ "\n")
86
87 val dir = wblDocDir ^ "/" ^ domain
88 val _ =
89 if Posix.FileSys.access (dir, []) then
90 ()
91 else
92 Posix.FileSys.mkdir (dir, Posix.FileSys.S.flags [Posix.FileSys.S.ixoth, Posix.FileSys.S.irwxu,
93 Posix.FileSys.S.irgrp, Posix.FileSys.S.iwgrp])
94
95 val htac = TextIO.openOut (dir ^ "/.htaccess")
96 val user = ref defaultUser
97 val group = ref defaultGroup
98
30ac0378
AC
99 val blocked = ref []
100
182a2654
AC
101 fun loop (line, ()) =
102 (case String.tokens Char.isSpace line of
103 [] => ()
104 | ["User", user'] =>
105 if StringSet.member (users, user') then
106 user := user'
107 else
108 Domtool.error (path, "not authorized to run as " ^ user')
109 | ["Group", group'] =>
110 if StringSet.member (groups, group') then
1e2e348e
AC
111 (group := group';
112 TextIO.output (loggroups, domain ^ "\t" ^ group' ^ "\n"))
182a2654
AC
113 else
114 Domtool.error (path, "not authorized to run as group " ^ group')
115 | ["ServerAdmin", email] => TextIO.output (vhosts, "\tServerAdmin " ^ email ^ "\n")
116 | ["UserDir"] => TextIO.output (vhosts, "\tUserDir public_html\n\t<Directory /home/*/public_html/cgi-bin>\n\t\tAllowOverride None\n\t\tOptions ExecCGI\n\t\tAllow from all\n\t\tSetHandler cgi-script\n\t</Directory>\n\tScriptAliasMatch ^/~(.*)/cgi-bin/(.*) /home/$1/public_html/cgi-bin/$2\n")
117 | ["DocumentRoot", p] =>
118 if checkPath (paths, p) then
119 TextIO.output (vhosts, "\tDocumentRoot " ^ p ^ "\n")
120 else
121 print (path ^ ": not authorized to use " ^ p ^ "\n")
05060d16
AC
122 | "RewriteRule" :: src :: dst :: rest =>
123 let
124 val flags =
125 case rest of
126 [] => SOME ""
127 | [flags] => if checkRewriteArgs (path, flags) then SOME flags else NONE
128 | _ => (Domtool.error (path, "Invalid mod_rewrite flags in " ^ chop line); NONE)
129 in
130 case flags of
131 SOME flags =>
132 (if not (!rewrite) then
133 (rewrite := true;
134 TextIO.output (vhosts, "\tRewriteEngine on\n"))
135 else
136 ();
137 TextIO.output (vhosts, "\tRewriteRule\t" ^ src ^ " " ^ dst ^ " " ^ flags ^ "\n"))
138 | _ => ()
139 end
140 | ["LocalProxy", src, dst, port] =>
141 (case Int.fromString port of
142 NONE => Domtool.error (path, "Invalid port number " ^ port)
143 | SOME n =>
144 if n = 80 then
145 Domtool.error (path, "No proxying back to Apache itself allowed")
146 else if n <= 0 then
147 Domtool.error (path, "Port number must be positive: " ^ port)
148 else
1e2e348e
AC
149 (if not (!rewrite) then
150 (rewrite := true;
151 TextIO.output (vhosts, "\tRewriteEngine on\n"))
152 else
153 ();
154 TextIO.output (vhosts, "\tRewriteRule\t" ^ src ^ " http://localhost:" ^ port ^ "/" ^ dst ^ " [P]\n")))
182a2654
AC
155 | ["Alias", from, to] =>
156 if checkPath (paths, to) then
157 TextIO.output (vhosts, "\tAlias " ^ from ^ " " ^ to ^ "\n")
158 else
159 Domtool.error (path, "not authorized to use " ^ to)
160 | "ErrorDocument" :: code :: rest =>
161 TextIO.output (vhosts, foldl (fn (a, s) => s ^ " " ^ a) ("\tErrorDocument " ^ code) rest ^ "\n")
162 | ["ScriptAlias", from, to] =>
163 if checkPath (paths, to) then
164 TextIO.output (vhosts, "\tScriptAlias " ^ from ^ " \"" ^ to ^ "\"\n")
165 else
166 Domtool.error (path, "not authorized to use " ^ to)
167 | ["SSI"] =>
168 TextIO.output (vhosts, "\t<Location />\n\t\tOptions +Includes +IncludesNOEXEC\n\t</Location>\n")
169 | ["ServerAlias", dom] =>
170 if validDomain dom then
171 let
172 val file = foldr (fn (c, s) => s ^ "/" ^ c) dataDir (String.fields (fn ch => ch = #".") dom) ^ ".aliased"
173 in
174 if Posix.FileSys.access (file, []) then
175 (TextIO.output (vhosts, "\tServerAlias " ^ dom ^ "\n");
176 TextIO.output (conf, "HideSite\t" ^ dom ^ "\n" ^
177 "HideReferrer\t" ^ dom ^ "\n"))
178 else
179 Domtool.error (path, "not authorized to ServerAlias " ^ dom)
180 end
181 else
182 Domtool.error (path, "bad host: " ^ dom)
183 | "WebalizerUsers" :: users =>
184 TextIO.output (htac, "AuthType Basic\n" ^
185 "AuthName \"Abulafia web account\"\n" ^
186 "AuthUserFile " ^ passwdFile ^ "\n" ^
187 foldl (fn (u, s) => s ^ " " ^ u) "Require user" users ^ "\n")
188 | ["AbuPrivate"] => TextIO.output (vhosts,
189 "\t<Location />\n" ^
190 "\t\tAuthName \"Abulafia web account\"\n" ^
191 "\t\tAuthType basic\n" ^
192 "\t\tAuthUserFile " ^ passwdFile ^ "\n" ^
193 "\t\tRequire valid-user\n" ^
194 "\t\tOrder Deny,Allow\n" ^
195 "\t\tDeny from all\n" ^
196 "\t\tAllow from 127.0.0.1\n" ^
197 "\t\tAllow from 63.246.10.45\n" ^
198 "\t\tSatisfy any\n" ^
199 "\t</Location>\n")
30ac0378 200 | ["Block", pat] => blocked := pat :: (!blocked)
182a2654
AC
201 | ["Default"] => (TextIO.output (vhosts, "\tServerAlias " ^ parent ^ "\n");
202 TextIO.output (conf, "HideSite\t" ^ parent ^ "\n" ^
203 "HideReferrer\t" ^ parent ^ "\n"))
204 | ["CGI", p] =>
205 if checkPath (paths, p) then
206 TextIO.output (vhosts, "\t<Directory " ^ p ^ ">\n" ^
207 "\t\tOptions ExecCGI\n" ^
208 "\t\tSetHandler cgi-script\n" ^
209 "\t</Directory>\n")
210 else
211 Domtool.error (path, "not authorized to use " ^ p)
212 | ["HTML", p] =>
213 if checkPath (paths, p) then
214 TextIO.output (vhosts, "\t<Directory " ^ p ^ ">\n" ^
215 "\t\tForceType text/html\n" ^
216 "\t</Directory>\n")
217 else
218 Domtool.error (path, "not authorized to use " ^ p)
219 | cmd::_ => Domtool.error (path, "unknown option: " ^ cmd))
220 in
221 TextIO.output (vhosts, "<VirtualHost *>\n" ^
222 "\tServerName " ^ domain ^ "\n" ^
223 "\tErrorLog " ^ logDir ^ domain ^ "-error.log\n" ^
224 "\tCustomLog " ^ logDir ^ domain ^ "-access.log combined\n" ^
225 "\tIndexOptions FancyIndexing FoldersFirst\n");
05060d16 226 ioOptLoop (fn () => Domtool.inputLine hf) loop ();
30ac0378
AC
227
228 (case !blocked of
229 [] => ()
230 | _ =>
231 (TextIO.output (vhosts,
232 "\t<Location />\n" ^
233 "\t\tOrder Allow,Deny\n" ^
234 "\t\tAllow from all\n");
235 app (fn pat => TextIO.output (vhosts, "\t\tDeny from " ^ pat ^ "\n")) (!blocked);
236 TextIO.output (vhosts, "\t</Location>\n")));
237
182a2654
AC
238 TextIO.output (vhosts, "\tUser ");
239 TextIO.output (vhosts, !user);
240 TextIO.output (vhosts, "\n\tGroup ");
241 TextIO.output (vhosts, !group);
242 TextIO.output (vhosts, "\n</VirtualHost>\n\n");
243 TextIO.closeIn hf;
244 TextIO.closeOut conf;
245 TextIO.closeOut htac
246 end handle Io => Domtool.error (path, "IO error")
247
248 fun publish () =
249 if OS.Process.isSuccess (OS.Process.system
250 (diff ^ " " ^ scratchDir ^ "/vhosts.conf " ^ dataFile)) then
251 OS.Process.success
252 else if not (OS.Process.isSuccess (OS.Process.system
253 (cp ^ " " ^ scratchDir ^ "/vhosts.conf " ^ dataFile))) then
254 (print "Error copying vhosts.conf\n";
255 OS.Process.failure)
1e2e348e
AC
256 else if not (OS.Process.isSuccess (OS.Process.system pubCommand)) then
257 (print "Error publishing vhosts.conf\n";
258 OS.Process.failure)
259 else if OS.Process.isSuccess (OS.Process.system logpermsCommand) then
182a2654
AC
260 OS.Process.success
261 else
1e2e348e 262 (print "Error updating log permissions\n";
182a2654
AC
263 OS.Process.failure)
264
265 fun mkdom _ = OS.Process.success
266
267 val _ = Domtool.setVhostHandler {init = init,
268 file = handler,
269 finish = finish,
270 publish = publish,
271 mkdom = mkdom}
272end
273