python: Added XBitHack-style caching to SSI handler.
[ashd.git] / python3 / ashd / ssi.py
CommitLineData
0b6f220f
FT
1"""Module for handling server-side-include formatted files
2
3This module is quite incomplete. I might complete it with more
4features as I need them. It will probably never be entirely compliant
5with Apache's version due to architectural differences.
6"""
7
8import sys, os, io, time, logging, functools
9from . import wsgiutil
10
11log = logging.getLogger("ssi")
12
13def parsecmd(text, p):
14 try:
15 while text[p].isspace(): p += 1
16 cmd = ""
17 while not text[p].isspace():
18 cmd += text[p]
19 p += 1
20 pars = {}
21 while True:
22 while text[p].isspace(): p += 1
23 if text[p:p + 3] == "-->":
24 return cmd, pars, p + 3
25 key = ""
26 while text[p].isalnum():
27 key += text[p]
28 p += 1
29 if key == "":
30 return None, {}, p
31 while text[p].isspace(): p += 1
32 if text[p] != '=':
33 continue
34 p += 1
35 while text[p].isspace(): p += 1
36 q = text[p]
37 if q != '"' and q != "'" and q != '`':
38 continue
39 val = ""
40 p += 1
41 while text[p] != q:
42 val += text[p]
43 p += 1
44 p += 1
45 pars[key] = val
46 except IndexError:
47 return None, {}, len(text)
48
49class context(object):
50 def __init__(self, out, root):
51 self.out = out
52 self.vars = {}
53 now = time.time()
54 self.vars["DOCUMENT_NAME"] = os.path.basename(root.path)
55 self.vars["DATE_GMT"] = time.asctime(time.gmtime(now))
56 self.vars["DATE_LOCAL"] = time.asctime(time.localtime(now))
57 self.vars["LAST_MODIFIED"] = time.asctime(time.localtime(root.mtime))
58
59class ssifile(object):
60 def __init__(self, path):
61 self.path = path
47da1b3d
FT
62 sb = os.stat(self.path)
63 self.cache = (sb.st_mode & 0o010) != 0
64 self.mtime = int(sb.st_mtime)
0b6f220f
FT
65 with open(path) as fp:
66 self.parts = self.parse(fp.read())
67
68 def text(self, text, ctx):
69 ctx.out.write(text)
70
71 def echo(self, var, enc, ctx):
72 if var in ctx.vars:
73 ctx.out.write(enc(ctx.vars[var]))
74
75 def include(self, path, ctx):
76 try:
77 nest = getfile(os.path.join(os.path.dirname(self.path), path))
78 except Exception:
79 log.warning("%s: could not find included file %s" % (self.path, path))
80 return
81 nest.process(ctx)
82
83 def process(self, ctx):
84 for part in self.parts:
85 part(ctx)
86
87 def resolvecmd(self, cmd, pars):
88 if cmd == "include":
89 if "file" in pars:
90 return functools.partial(self.include, pars["file"])
91 elif "virtual" in pars:
92 # XXX: For now, just include the file as-is. Change
93 # when necessary.
94 return functools.partial(self.include, pars["virtual"])
95 else:
96 log.warning("%s: invalid `include' directive" % self.path)
97 return None
98 elif cmd == "echo":
99 if not "var" in pars:
100 log.warning("%s: invalid `echo' directive" % self.path)
101 return None
102 enc = wsgiutil.htmlquote
103 if "encoding" in pars:
104 if pars["encoding"] == "entity":
105 enc = wsgiutil.htmlquote
106 return functools.partial(self.echo, pars["var"], enc)
107 else:
108 log.warning("%s: unknown SSI command `%s'" % (self.path, cmd))
109 return None
110
111 def parse(self, text):
112 ret = []
113 p = 0
114 while True:
115 p2 = text.find("<!--#", p)
116 if p2 < 0:
117 ret.append(functools.partial(self.text, text[p:]))
118 return ret
119 ret.append(functools.partial(self.text, text[p:p2]))
120 cmd, pars, p = parsecmd(text, p2 + 5)
121 if cmd is not None:
122 cmd = self.resolvecmd(cmd, pars)
123 if cmd is not None:
124 ret.append(cmd)
125
126filecache = {}
127
128def getfile(path):
129 path = os.path.normpath(path)
130 cf = filecache.get(path)
131 if not cf:
132 cf = filecache[path] = ssifile(path)
47da1b3d 133 elif int(os.stat(path).st_mtime) != cf.mtime:
0b6f220f
FT
134 cf = filecache[path] = ssifile(path)
135 return cf
136
137def wsgi(env, startreq):
138 try:
139 if env["PATH_INFO"] != "":
140 return wsgiutil.simpleerror(env, startreq, 404, "Not Found", "The resource specified by the URL does not exist.")
141 root = getfile(env["SCRIPT_FILENAME"])
47da1b3d
FT
142
143 if root.cache and "HTTP_IF_MODIFIED_SINCE" in env:
144 try:
145 lmt = wsgiutil.phttpdate(env["HTTP_IF_MODIFIED_SINCE"])
146 if root.mtime <= lmt:
147 startreq("304 Not Modified", [("Content-Length", "0")])
148 return []
149 except:
150 pass
151
0b6f220f
FT
152 buf = io.StringIO()
153 root.process(context(buf, root))
154 except Exception:
47da1b3d 155 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server encountered an unpexpected error while handling SSI.")
0b6f220f 156 ret = buf.getvalue().encode("utf8")
47da1b3d
FT
157 head = [("Content-Type", "text/html; charset=UTF-8"), ("Content-Length", str(len(ret)))]
158 if root.cache:
159 head.append(("Last-Modified", wsgiutil.httpdate(root.mtime)))
160 startreq("200 OK", head)
0b6f220f 161 return [ret]