Fix mm_cfg.py order bug
[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 36 val noargs = ["redirect", "R", "forbidden", "F", "gone", "G", "last", "L", "chain", "C", "nosubeq", "NS", "nocase", "NC", "qsappend", "QSA", "noescape", "NE", "passthrough", "PT"]
0e2e70f2
AC
37
38 fun checkRewriteCondArgs (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 checkOne f =
47 case f of
48 "nocase" => true
49 | "NC" => true
50 | "ornext" => true
51 | "OR" => true
52 | _ => false
53 in
54 List.all checkOne fields
55 end
56
57
05060d16
AC
58 fun checkRewriteArgs (path, args) =
59 if size args < 2 orelse String.sub (args, 0) <> #"[" orelse String.sub (args, size args - 1) <> #"]" then
60 (Domtool.error (path, "Not in brackets: " ^ args);
61 false)
62 else let
63 val args = String.substring (args, 1, size args - 2)
64 val fields = String.fields (fn ch => ch = #",") args
65
66 fun checkField f =
67 case String.fields (fn ch => ch = #"=") f of
68 [flag] => List.exists (fn x => x = flag) noargs orelse (Domtool.error (path, "Unknown argument-free flag " ^ flag);
69 false)
05060d16
AC
70 | ["type", _] => true
71 | ["T", _] => true
72 | ["skip", num] => isNat num orelse (Domtool.error (path, "Bad skip number " ^ num); false)
73 | ["S", num] => isNat num orelse (Domtool.error (path, "Bad skip number " ^ num); false)
74 | ["env", varval] =>
75 (case String.fields (fn ch => ch = #":") varval of
76 [_, _] => true
77 | _ => (Domtool.error (path, "Bad env setting " ^ varval);
78 false))
79 | ["E", varval] =>
80 (case String.fields (fn ch => ch = #":") varval of
81 [_, _] => true
82 | _ => (Domtool.error (path, "Bad env setting " ^ varval);
83 false))
84 | _ => (Domtool.error (path, "Unknown or disallowed mod_rewrite flag " ^ f);
85 false)
86 in
87 List.all checkField fields
88 end
89
874b616a 90 fun handler {path, domain, parent, vars, paths, users, groups, mxs} =
182a2654
AC
91 let
92 val _ = Domtool.dprint ("Reading host " ^ path ^ " for " ^ domain ^ "....")
93
94 val vhosts = valOf (!vhosts)
1e2e348e 95 val loggroups = valOf (!loggroups)
182a2654 96
874b616a
AC
97 val domfile = "/etc/domains/" ^ String.concatWith "/" (rev (String.tokens (fn ch => ch = #".") domain))
98 val stat = Posix.FileSys.stat domfile
99 val group' = Posix.SysDB.Group.name (Posix.SysDB.getgrgid (Posix.FileSys.ST.gid stat))
100
101 val _ = TextIO.output (loggroups, domain ^ "\t" ^ group' ^ "\n")
102
182a2654
AC
103 val hf = TextIO.openIn path
104 val rewrite = ref false
105
106 val conf = TextIO.openOut (wblConfDir ^ "/" ^ domain ^ ".conf")
107 val _ = TextIO.output (conf, "LogFile\t" ^ logDir ^ domain ^ "-access.log\n" ^
108 "OutputDir\t" ^ wblDocDir ^ "/" ^ domain ^ "\n" ^
109 "HostName\t" ^ domain ^ "\n" ^
110 "HideSite\t" ^ domain ^ "\n" ^
111 "HideReferrer\t" ^ domain ^ "\n")
112
113 val dir = wblDocDir ^ "/" ^ domain
114 val _ =
115 if Posix.FileSys.access (dir, []) then
116 ()
117 else
118 Posix.FileSys.mkdir (dir, Posix.FileSys.S.flags [Posix.FileSys.S.ixoth, Posix.FileSys.S.irwxu,
119 Posix.FileSys.S.irgrp, Posix.FileSys.S.iwgrp])
120
121 val htac = TextIO.openOut (dir ^ "/.htaccess")
122 val user = ref defaultUser
123 val group = ref defaultGroup
124
30ac0378
AC
125 val blocked = ref []
126
182a2654
AC
127 fun loop (line, ()) =
128 (case String.tokens Char.isSpace line of
129 [] => ()
130 | ["User", user'] =>
131 if StringSet.member (users, user') then
132 user := user'
133 else
134 Domtool.error (path, "not authorized to run as " ^ user')
135 | ["Group", group'] =>
136 if StringSet.member (groups, group') then
874b616a 137 group := group'
182a2654 138 else
874b616a 139 Domtool.error (path, "not authorized to run as group " ^ group')
182a2654
AC
140 | ["ServerAdmin", email] => TextIO.output (vhosts, "\tServerAdmin " ^ email ^ "\n")
141 | ["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")
142 | ["DocumentRoot", p] =>
143 if checkPath (paths, p) then
144 TextIO.output (vhosts, "\tDocumentRoot " ^ p ^ "\n")
145 else
146 print (path ^ ": not authorized to use " ^ p ^ "\n")
05060d16
AC
147 | "RewriteRule" :: src :: dst :: rest =>
148 let
149 val flags =
150 case rest of
151 [] => SOME ""
152 | [flags] => if checkRewriteArgs (path, flags) then SOME flags else NONE
153 | _ => (Domtool.error (path, "Invalid mod_rewrite flags in " ^ chop line); NONE)
154 in
155 case flags of
156 SOME flags =>
157 (if not (!rewrite) then
158 (rewrite := true;
159 TextIO.output (vhosts, "\tRewriteEngine on\n"))
160 else
161 ();
162 TextIO.output (vhosts, "\tRewriteRule\t" ^ src ^ " " ^ dst ^ " " ^ flags ^ "\n"))
163 | _ => ()
164 end
0e2e70f2
AC
165 | "RewriteCond" :: thing :: pat :: rest =>
166 let
167 val flags =
168 case rest of
169 [] => SOME ""
170 | [flags] => if checkRewriteCondArgs (path, flags) then SOME flags else NONE
171 | _ => (Domtool.error (path, "Invalid mod_rewrite flags in " ^ chop line); NONE)
172 in
173 case flags of
174 SOME flags =>
175 (if not (!rewrite) then
176 (rewrite := true;
177 TextIO.output (vhosts, "\tRewriteEngine on\n"))
178 else
179 ();
180 TextIO.output (vhosts, "\tRewriteCond\t" ^ thing ^ " " ^ pat ^ " " ^ flags ^ "\n"))
181 | _ => ()
182 end
05060d16
AC
183 | ["LocalProxy", src, dst, port] =>
184 (case Int.fromString port of
185 NONE => Domtool.error (path, "Invalid port number " ^ port)
186 | SOME n =>
187 if n = 80 then
188 Domtool.error (path, "No proxying back to Apache itself allowed")
189 else if n <= 0 then
190 Domtool.error (path, "Port number must be positive: " ^ port)
191 else
1e2e348e
AC
192 (if not (!rewrite) then
193 (rewrite := true;
194 TextIO.output (vhosts, "\tRewriteEngine on\n"))
195 else
196 ();
197 TextIO.output (vhosts, "\tRewriteRule\t" ^ src ^ " http://localhost:" ^ port ^ "/" ^ dst ^ " [P]\n")))
182a2654
AC
198 | ["Alias", from, to] =>
199 if checkPath (paths, to) then
200 TextIO.output (vhosts, "\tAlias " ^ from ^ " " ^ to ^ "\n")
201 else
202 Domtool.error (path, "not authorized to use " ^ to)
203 | "ErrorDocument" :: code :: rest =>
204 TextIO.output (vhosts, foldl (fn (a, s) => s ^ " " ^ a) ("\tErrorDocument " ^ code) rest ^ "\n")
205 | ["ScriptAlias", from, to] =>
206 if checkPath (paths, to) then
207 TextIO.output (vhosts, "\tScriptAlias " ^ from ^ " \"" ^ to ^ "\"\n")
208 else
209 Domtool.error (path, "not authorized to use " ^ to)
210 | ["SSI"] =>
211 TextIO.output (vhosts, "\t<Location />\n\t\tOptions +Includes +IncludesNOEXEC\n\t</Location>\n")
212 | ["ServerAlias", dom] =>
213 if validDomain dom then
214 let
215 val file = foldr (fn (c, s) => s ^ "/" ^ c) dataDir (String.fields (fn ch => ch = #".") dom) ^ ".aliased"
216 in
217 if Posix.FileSys.access (file, []) then
218 (TextIO.output (vhosts, "\tServerAlias " ^ dom ^ "\n");
219 TextIO.output (conf, "HideSite\t" ^ dom ^ "\n" ^
220 "HideReferrer\t" ^ dom ^ "\n"))
221 else
222 Domtool.error (path, "not authorized to ServerAlias " ^ dom)
223 end
224 else
225 Domtool.error (path, "bad host: " ^ dom)
226 | "WebalizerUsers" :: users =>
227 TextIO.output (htac, "AuthType Basic\n" ^
228 "AuthName \"Abulafia web account\"\n" ^
229 "AuthUserFile " ^ passwdFile ^ "\n" ^
230 foldl (fn (u, s) => s ^ " " ^ u) "Require user" users ^ "\n")
231 | ["AbuPrivate"] => TextIO.output (vhosts,
232 "\t<Location />\n" ^
233 "\t\tAuthName \"Abulafia web account\"\n" ^
234 "\t\tAuthType basic\n" ^
235 "\t\tAuthUserFile " ^ passwdFile ^ "\n" ^
236 "\t\tRequire valid-user\n" ^
237 "\t\tOrder Deny,Allow\n" ^
238 "\t\tDeny from all\n" ^
239 "\t\tAllow from 127.0.0.1\n" ^
240 "\t\tAllow from 63.246.10.45\n" ^
241 "\t\tSatisfy any\n" ^
242 "\t</Location>\n")
30ac0378 243 | ["Block", pat] => blocked := pat :: (!blocked)
182a2654
AC
244 | ["Default"] => (TextIO.output (vhosts, "\tServerAlias " ^ parent ^ "\n");
245 TextIO.output (conf, "HideSite\t" ^ parent ^ "\n" ^
246 "HideReferrer\t" ^ parent ^ "\n"))
247 | ["CGI", p] =>
248 if checkPath (paths, p) then
249 TextIO.output (vhosts, "\t<Directory " ^ p ^ ">\n" ^
250 "\t\tOptions ExecCGI\n" ^
251 "\t\tSetHandler cgi-script\n" ^
252 "\t</Directory>\n")
253 else
254 Domtool.error (path, "not authorized to use " ^ p)
874b616a
AC
255 | ["Mod", lang, p, file] =>
256 (case List.find (fn (lang', _) => lang = lang') langHandlers of
257 NONE => Domtool.error (p, "unknown Mod language " ^ lang)
258 | SOME (_, f) =>
259 (TextIO.output (vhosts, "\t<Location " ^ p ^ ">\n");
260 TextIO.output (vhosts, f file);
261 TextIO.output (vhosts, "\t</Location>\n")))
182a2654
AC
262 | ["HTML", p] =>
263 if checkPath (paths, p) then
264 TextIO.output (vhosts, "\t<Directory " ^ p ^ ">\n" ^
265 "\t\tForceType text/html\n" ^
266 "\t</Directory>\n")
267 else
268 Domtool.error (path, "not authorized to use " ^ p)
874b616a
AC
269 | ["PerlSetVar", n, v] =>
270 TextIO.output (vhosts, "\tPerlSetVar " ^ n ^ " " ^ v ^ "\n")
271 | ["AddDefaultCharset", cs] =>
272 TextIO.output (vhosts, "\tAddDefaultCharSet " ^ cs ^ "\n")
182a2654
AC
273 | cmd::_ => Domtool.error (path, "unknown option: " ^ cmd))
274 in
275 TextIO.output (vhosts, "<VirtualHost *>\n" ^
276 "\tServerName " ^ domain ^ "\n" ^
277 "\tErrorLog " ^ logDir ^ domain ^ "-error.log\n" ^
278 "\tCustomLog " ^ logDir ^ domain ^ "-access.log combined\n" ^
279 "\tIndexOptions FancyIndexing FoldersFirst\n");
05060d16 280 ioOptLoop (fn () => Domtool.inputLine hf) loop ();
30ac0378
AC
281
282 (case !blocked of
283 [] => ()
284 | _ =>
285 (TextIO.output (vhosts,
286 "\t<Location />\n" ^
287 "\t\tOrder Allow,Deny\n" ^
288 "\t\tAllow from all\n");
289 app (fn pat => TextIO.output (vhosts, "\t\tDeny from " ^ pat ^ "\n")) (!blocked);
290 TextIO.output (vhosts, "\t</Location>\n")));
291
182a2654
AC
292 TextIO.output (vhosts, "\tUser ");
293 TextIO.output (vhosts, !user);
294 TextIO.output (vhosts, "\n\tGroup ");
295 TextIO.output (vhosts, !group);
296 TextIO.output (vhosts, "\n</VirtualHost>\n\n");
297 TextIO.closeIn hf;
298 TextIO.closeOut conf;
299 TextIO.closeOut htac
300 end handle Io => Domtool.error (path, "IO error")
301
302 fun publish () =
303 if OS.Process.isSuccess (OS.Process.system
304 (diff ^ " " ^ scratchDir ^ "/vhosts.conf " ^ dataFile)) then
305 OS.Process.success
306 else if not (OS.Process.isSuccess (OS.Process.system
307 (cp ^ " " ^ scratchDir ^ "/vhosts.conf " ^ dataFile))) then
308 (print "Error copying vhosts.conf\n";
309 OS.Process.failure)
1e2e348e
AC
310 else if not (OS.Process.isSuccess (OS.Process.system pubCommand)) then
311 (print "Error publishing vhosts.conf\n";
312 OS.Process.failure)
313 else if OS.Process.isSuccess (OS.Process.system logpermsCommand) then
182a2654
AC
314 OS.Process.success
315 else
1e2e348e 316 (print "Error updating log permissions\n";
182a2654
AC
317 OS.Process.failure)
318
319 fun mkdom _ = OS.Process.success
320
321 val _ = Domtool.setVhostHandler {init = init,
322 file = handler,
323 finish = finish,
324 publish = publish,
325 mkdom = mkdom}
326end
327