X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/chopwood/blobdiff_plain/a2916c0635fec5b45ad742904db9f5769b48f53d..1f8350d2e314e9497fd58c11a125f15b51e98238:/cgi.py
diff --git a/cgi.py b/cgi.py
index f69646c..69b9038 100644
--- a/cgi.py
+++ b/cgi.py
@@ -59,7 +59,7 @@ CONF.DEFAULTS.update(
## Some handy regular expressions.
R_URLESC = RX.compile('%([0-9a-fA-F]{2})')
R_URLBAD = RX.compile('[^-\\w,.!]')
-R_HTMLBAD = RX.compile('[&<>]')
+R_HTMLBAD = RX.compile('[&<>\'"]')
def urldecode(s):
"""Decode a single form-url-encoded string S."""
@@ -77,17 +77,18 @@ def htmlescape(s):
## Some standard character sequences, and HTML entity names for prettier
## versions.
-_quotify = U.StringSubst({
+html_quotify = U.StringSubst({
+ "<": '<',
+ ">": '>',
+ "&": '&',
"`": '‘',
"'": '’',
+ '"': '"',
"``": '“',
"''": '”',
"--": '–',
"---": '—'
})
-def html_quotify(s):
- """Return a pretty HTML version of S."""
- return _quotify(htmlescape(s))
###--------------------------------------------------------------------------
### Output machinery.
@@ -114,7 +115,7 @@ class HTTPOutput (O.FileOutput):
"""
Print a header, if none has yet been printed.
- Keyword arguments can be passed to emit HTTP headers: see `http_header'
+ Keyword arguments can be passed to emit HTTP headers: see `http_headers'
for the formatting rules.
"""
if me.headerp: return
@@ -122,6 +123,8 @@ class HTTPOutput (O.FileOutput):
for h in O.http_headers(content_type = content_type, **kw):
me.writeln(h)
me.writeln('')
+ if METHOD == 'HEAD':
+ HEADER_DONE()
def cookie(name, value, **kw):
"""
@@ -145,7 +148,7 @@ def cookie(name, value, **kw):
T.gmtime(U.NOW + maxage))
return '; '.join(['%s=%s' % (urlencode(name), urlencode(value))] +
[v is not True and '%s=%s' % (k, v) or k
- for k, v in attr.iteritems()])
+ for k, v in attr.iteritems() if v])
def action(*v, **kw):
"""
@@ -166,47 +169,6 @@ def static(name):
"""Build a URL for the static file NAME."""
return htmlescape(CFG.STATIC + '/' + name)
-@CTX.contextmanager
-def html(title, **kw):
- """
- Context manager for HTML output.
-
- Keyword arguments are output as HTTP headers (if no header has been written
- yet). A `
' element is written, and a `' opened, before the
- context body is executed; the elements are closed off properly at the end.
- """
-
- kw = dict(kw, content_type = 'text/html')
- OUT.header(**kw)
-
- ## Write the HTML header.
- PRINT("""\
-
-
-
- %(title)s
-
-
-
-""" % dict(title = html_quotify(title),
- style = static('chpwd.css'),
- script = static('chpwd.js')))
-
- ## Write the body.
- PRINT('')
- yield None
- PRINT('''\
-
-
-
-
-''' % dict(about = static('about.html'),
- version = VERSION))
-
def redirect(where, **kw):
"""
Write a complete redirection to some other URL.
@@ -237,7 +199,8 @@ def set_template_keywords():
package = PACKAGE,
version = VERSION,
script = CFG.SCRIPT_NAME,
- static = CFG.STATIC)
+ static = CFG.STATIC,
+ allowop = CFG.ALLOWOP)
class TemplateFinder (object):
"""
@@ -252,7 +215,7 @@ class TemplateFinder (object):
with open(OS.path.join(me._dir, key)) as f: tmpl = f.read()
me._cache[key] = tmpl
return tmpl
-TMPL = TemplateFinder(TMPLDIR)
+STATE.kw['TMPL'] = TMPL = TemplateFinder(TMPLDIR)
@CTX.contextmanager
def tmplkw(**kw):
@@ -270,13 +233,36 @@ class FormatHTML (F.SimpleFormatOperation):
"""
~H: escape output suitable for inclusion in HTML.
- With `:', instead apply form-urlencoding.
+ With `:', additionally apply quotification.
"""
def _convert(me, arg):
if me.colonp: return html_quotify(arg)
else: return htmlescape(arg)
FORMATOPS['H'] = FormatHTML
+class FormatWrap (F.BaseFormatOperation):
+ """
+ ~<...~@>: wrap enclosed material in another formatting control string.
+
+ The argument is a formatting control. The enclosed material is split into
+ pieces separated by `~;' markers. The formatting control is performed, and
+ passed the list of pieces (as compiled formatting operations) in the
+ keyword argument `wrapped'.
+ """
+ def __init__(me, *args):
+ super(FormatWrap, me).__init__(*args)
+ pieces = []
+ while True:
+ piece, delim = F.collect_subformat('>;')
+ pieces.append(piece)
+ if delim.char == '>': break
+ me.pieces = pieces
+ def _format(me, atp, colonp):
+ op = F.compile(me.getarg.get())
+ with F.FORMAT.bind(argmap = dict(F.FORMAT.argmap, wrapped = me.pieces)):
+ op.format()
+FORMATOPS['<'] = FormatWrap
+
def format_tmpl(control, **kw):
with F.COMPILE.bind(opmaps = [FORMATOPS, F.BASEOPS]):
with tmplkw(**kw):
@@ -291,74 +277,6 @@ def page(template, header = {}, title = 'Chopwood', **kw):
###--------------------------------------------------------------------------
### Error reporting.
-def cgi_error_guts():
- """
- Report an exception while we're acting as a CGI, together with lots of
- information about our state.
-
- Our caller has, probably at great expense, arranged that we can format lots
- of text.
- """
-
- ## Grab the exception information.
- exty, exval, extb = SYS.exc_info()
-
- ## Print the exception itself.
- PRINT("""\
-
Exception
-
%s
""" % html_quotify(
- '\n'.join(TB.format_exception_only(exty, exval))))
-
- ## Format a traceback so we can find out what has gone wrong.
- PRINT("""\
-
Traceback
-""")
- for file, line, func, text in TB.extract_tb(extb, 20):
- PRINT("
%s:%d (%s)" % (
- htmlescape(file), line, htmlescape(func)))
- if text is not None:
- PRINT(" %s" % htmlescape(text))
- PRINT("
")
-
- ## Format various useful tables.
- def fmt_dict(d):
- fmt_kvlist(d.iteritems())
- def fmt_kvlist(l):
- for k, v in sorted(l):
- PRINT("
%s
%s" % (
- htmlescape(k), htmlescape(v)))
- def fmt_list(l):
- for i in l:
- PRINT("
%s" % htmlescape(i))
-
- PRINT("""\
-
Parameters
""")
- for what, thing, how in [('Query', PARAM, fmt_kvlist),
- ('Cookies', COOKIE, fmt_dict),
- ('Path', PATH, fmt_list),
- ('Environment', ENV, fmt_dict)]:
- PRINT("
%s
\n
" % what)
- how(thing)
- PRINT("
")
-
-def cgi_error():
- """
- Report an exception while in CGI mode.
-
- If we've not produced a header yet, then we can do that, and produce a
- status code and everything; otherwise we'll have to make do with a small
- piece of the page.
- """
- if OUT.headerp:
- PRINT("
")
- cgi_error_guts()
- PRINT("
\n")
- else:
- with html("chpwd internal error", status = 500):
- PRINT("
chpwd internal error
")
- cgi_error_guts()
- SYS.exit(1)
-
@CTX.contextmanager
def cgi_errors(hook = None):
"""
@@ -373,7 +291,7 @@ def cgi_errors(hook = None):
if hook: hook()
if isinstance(e, U.ExpectedError) and not OUT.headerp:
page('error.fhtml',
- headers = dict(status = e.code),
+ header = dict(status = e.code),
title = 'Chopwood: error', error = e)
else:
exty, exval, extb = SYS.exc_info()
@@ -387,7 +305,7 @@ def cgi_errors(hook = None):
format_tmpl(TMPL['exception.fhtml'], toplevel = False)
else:
page('exception.fhtml',
- headers = dict(status = 500),
+ header = dict(status = 500),
title = 'Chopwood: internal error',
toplevel = True)
@@ -395,11 +313,14 @@ def cgi_errors(hook = None):
### CGI input.
## Lots of global variables to be filled in by `cgiparse'.
+METHOD = None
COOKIE = {}
SPECIAL = {}
PARAM = []
PARAMDICT = {}
PATH = []
+SSLP = False
+HEADER_DONE = lambda: None
## Regular expressions for splitting apart query and cookie strings.
R_QSPLIT = RX.compile('[;&]')
@@ -455,34 +376,40 @@ def cgiparse():
`PATH'
The trailing `PATH_INFO' path, split at `/' markers, with any
trailing empty component removed.
+
+ `SSLP'
+ True if the client connection is carried over SSL or TLS.
"""
+ global METHOD, SSLP
+
def getenv(var):
try: return ENV[var]
except KeyError: raise U.ExpectedError, (500, "No `%s' supplied" % var)
## Yes, we want the request method.
- method = getenv('REQUEST_METHOD')
+ METHOD = getenv('REQUEST_METHOD')
## Acquire the query string.
- if method == 'GET':
+ if METHOD in ['GET', 'HEAD']:
q = getenv('QUERY_STRING')
- elif method == 'POST':
+ elif METHOD == 'POST':
## We must read the query string from stdin.
n = getenv('CONTENT_LENGTH')
if not n.isdigit():
raise U.ExpectedError, (500, "Invalid CONTENT_LENGTH")
n = int(n, 10)
- if getenv('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
+ ct = getenv('CONTENT_TYPE')
+ if ct != 'application/x-www-form-urlencoded':
raise U.ExpectedError, (500, "Unexpected content type `%s'" % ct)
q = SYS.stdin.read(n)
if len(q) != n:
raise U.ExpectedError, (500, "Failed to read correct length")
else:
- raise U.ExpectedError, (500, "Unexpected request method `%s'" % method)
+ raise U.ExpectedError, (500, "Unexpected request method `%s'" % METHOD)
## Populate the `SPECIAL', `PARAM' and `PARAMDICT' tables.
seen = set()
@@ -492,7 +419,8 @@ def cgiparse():
else:
PARAM.append((k, v))
if k in seen:
- del PARAMDICT[k]
+ try: del PARAMDICT[k]
+ except KeyError: pass
else:
PARAMDICT[k] = v
seen.add(k)
@@ -511,6 +439,10 @@ def cgiparse():
if pp and not pp[-1]: pp.pop()
PATH[:] = pp
+ ## Check the crypto for the connection.
+ if ENV.get('SSL_PROTOCOL'):
+ SSLP = True
+
###--------------------------------------------------------------------------
### CGI subcommands.
@@ -522,6 +454,11 @@ class Subcommand (SC.Subcommand):
CGI parameters.
"""
+ def __init__(me, name, contexts, desc, func,
+ methods = ['GET', 'POST'], *args, **kw):
+ super(Subcommand, me).__init__(name, contexts, desc, func, *args, **kw)
+ me.methods = set(methods)
+
def cgi(me, param, path):
"""
Invoke the subcommand given a collection of CGI parameters.
@@ -538,6 +475,8 @@ class Subcommand (SC.Subcommand):
the list of path elements is non-empty.
"""
+ global HEADER_DONE
+
## We're going to make a pass over the supplied parameters, and we'll
## check them off against the formal parameters as we go; so we'll need
## to be able to look them up. We'll also keep track of the ones we've
@@ -551,6 +490,12 @@ class Subcommand (SC.Subcommand):
want = {}
kw = {}
+ ## Check the request method against the permitted list.
+ meth = METHOD
+ if meth == 'HEAD': meth = 'GET'
+ if meth not in me.methods:
+ raise U.ExpectedError, (500, "Unexpected request method `%s'" % METHOD)
+
def set_value(k, v):
"""Set a simple value: we shouldn't see multiple values."""
if k in kw: