X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/chopwood/blobdiff_plain/a2916c0635fec5b45ad742904db9f5769b48f53d..76ee7d4f64bc1f20013f44c8045cd708ef0b9641:/cgi.py diff --git a/cgi.py b/cgi.py index f69646c..6006062 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('''\ - -
- Chopwood, version %(version)s: - copyright © 2012 Mark Wooding -
- - -''' % 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("
  1. %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() @@ -511,6 +438,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. @@ -538,6 +469,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