##// END OF EJS Templates
hgweb: support custom http headers in ErrorResponse
Sune Foldager -
r7741:a3d7f99c default
parent child Browse files
Show More
@@ -1,123 +1,124 b''
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_BAD_REQUEST = 400
12 HTTP_BAD_REQUEST = 400
13 HTTP_UNAUTHORIZED = 401
13 HTTP_UNAUTHORIZED = 401
14 HTTP_FORBIDDEN = 403
14 HTTP_FORBIDDEN = 403
15 HTTP_NOT_FOUND = 404
15 HTTP_NOT_FOUND = 404
16 HTTP_METHOD_NOT_ALLOWED = 405
16 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_SERVER_ERROR = 500
17 HTTP_SERVER_ERROR = 500
18
18
19 class ErrorResponse(Exception):
19 class ErrorResponse(Exception):
20 def __init__(self, code, message=None):
20 def __init__(self, code, message=None, headers=[]):
21 Exception.__init__(self)
21 Exception.__init__(self)
22 self.code = code
22 self.code = code
23 self.headers = headers
23 if message is not None:
24 if message is not None:
24 self.message = message
25 self.message = message
25 else:
26 else:
26 self.message = _statusmessage(code)
27 self.message = _statusmessage(code)
27
28
28 def _statusmessage(code):
29 def _statusmessage(code):
29 from BaseHTTPServer import BaseHTTPRequestHandler
30 from BaseHTTPServer import BaseHTTPRequestHandler
30 responses = BaseHTTPRequestHandler.responses
31 responses = BaseHTTPRequestHandler.responses
31 return responses.get(code, ('Error', 'Unknown error'))[0]
32 return responses.get(code, ('Error', 'Unknown error'))[0]
32
33
33 def statusmessage(code):
34 def statusmessage(code):
34 return '%d %s' % (code, _statusmessage(code))
35 return '%d %s' % (code, _statusmessage(code))
35
36
36 def get_mtime(repo_path):
37 def get_mtime(repo_path):
37 store_path = os.path.join(repo_path, ".hg")
38 store_path = os.path.join(repo_path, ".hg")
38 if not os.path.isdir(os.path.join(store_path, "data")):
39 if not os.path.isdir(os.path.join(store_path, "data")):
39 store_path = os.path.join(store_path, "store")
40 store_path = os.path.join(store_path, "store")
40 cl_path = os.path.join(store_path, "00changelog.i")
41 cl_path = os.path.join(store_path, "00changelog.i")
41 if os.path.exists(cl_path):
42 if os.path.exists(cl_path):
42 return os.stat(cl_path).st_mtime
43 return os.stat(cl_path).st_mtime
43 else:
44 else:
44 return os.stat(store_path).st_mtime
45 return os.stat(store_path).st_mtime
45
46
46 def staticfile(directory, fname, req):
47 def staticfile(directory, fname, req):
47 """return a file inside directory with guessed Content-Type header
48 """return a file inside directory with guessed Content-Type header
48
49
49 fname always uses '/' as directory separator and isn't allowed to
50 fname always uses '/' as directory separator and isn't allowed to
50 contain unusual path components.
51 contain unusual path components.
51 Content-Type is guessed using the mimetypes module.
52 Content-Type is guessed using the mimetypes module.
52 Return an empty string if fname is illegal or file not found.
53 Return an empty string if fname is illegal or file not found.
53
54
54 """
55 """
55 parts = fname.split('/')
56 parts = fname.split('/')
56 for part in parts:
57 for part in parts:
57 if (part in ('', os.curdir, os.pardir) or
58 if (part in ('', os.curdir, os.pardir) or
58 os.sep in part or os.altsep is not None and os.altsep in part):
59 os.sep in part or os.altsep is not None and os.altsep in part):
59 return ""
60 return ""
60 fpath = os.path.join(*parts)
61 fpath = os.path.join(*parts)
61 if isinstance(directory, str):
62 if isinstance(directory, str):
62 directory = [directory]
63 directory = [directory]
63 for d in directory:
64 for d in directory:
64 path = os.path.join(d, fpath)
65 path = os.path.join(d, fpath)
65 if os.path.exists(path):
66 if os.path.exists(path):
66 break
67 break
67 try:
68 try:
68 os.stat(path)
69 os.stat(path)
69 ct = mimetypes.guess_type(path)[0] or "text/plain"
70 ct = mimetypes.guess_type(path)[0] or "text/plain"
70 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
71 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
71 return file(path, 'rb').read()
72 return file(path, 'rb').read()
72 except TypeError:
73 except TypeError:
73 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal file name')
74 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal file name')
74 except OSError, err:
75 except OSError, err:
75 if err.errno == errno.ENOENT:
76 if err.errno == errno.ENOENT:
76 raise ErrorResponse(HTTP_NOT_FOUND)
77 raise ErrorResponse(HTTP_NOT_FOUND)
77 else:
78 else:
78 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
79 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
79
80
80 def style_map(templatepath, style):
81 def style_map(templatepath, style):
81 """Return path to mapfile for a given style.
82 """Return path to mapfile for a given style.
82
83
83 Searches mapfile in the following locations:
84 Searches mapfile in the following locations:
84 1. templatepath/style/map
85 1. templatepath/style/map
85 2. templatepath/map-style
86 2. templatepath/map-style
86 3. templatepath/map
87 3. templatepath/map
87 """
88 """
88 locations = style and [os.path.join(style, "map"), "map-"+style] or []
89 locations = style and [os.path.join(style, "map"), "map-"+style] or []
89 locations.append("map")
90 locations.append("map")
90 if isinstance(templatepath, str):
91 if isinstance(templatepath, str):
91 templatepath = [templatepath]
92 templatepath = [templatepath]
92 for path in templatepath:
93 for path in templatepath:
93 for location in locations:
94 for location in locations:
94 mapfile = os.path.join(path, location)
95 mapfile = os.path.join(path, location)
95 if os.path.isfile(mapfile):
96 if os.path.isfile(mapfile):
96 return mapfile
97 return mapfile
97 raise RuntimeError("No hgweb templates found in %r" % templatepath)
98 raise RuntimeError("No hgweb templates found in %r" % templatepath)
98
99
99 def paritygen(stripecount, offset=0):
100 def paritygen(stripecount, offset=0):
100 """count parity of horizontal stripes for easier reading"""
101 """count parity of horizontal stripes for easier reading"""
101 if stripecount and offset:
102 if stripecount and offset:
102 # account for offset, e.g. due to building the list in reverse
103 # account for offset, e.g. due to building the list in reverse
103 count = (stripecount + offset) % stripecount
104 count = (stripecount + offset) % stripecount
104 parity = (stripecount + offset) / stripecount & 1
105 parity = (stripecount + offset) / stripecount & 1
105 else:
106 else:
106 count = 0
107 count = 0
107 parity = 0
108 parity = 0
108 while True:
109 while True:
109 yield parity
110 yield parity
110 count += 1
111 count += 1
111 if stripecount and count >= stripecount:
112 if stripecount and count >= stripecount:
112 parity = 1 - parity
113 parity = 1 - parity
113 count = 0
114 count = 0
114
115
115 def get_contact(config):
116 def get_contact(config):
116 """Return repo contact information or empty string.
117 """Return repo contact information or empty string.
117
118
118 web.contact is the primary source, but if that is not set, try
119 web.contact is the primary source, but if that is not set, try
119 ui.username or $EMAIL as a fallback to display something useful.
120 ui.username or $EMAIL as a fallback to display something useful.
120 """
121 """
121 return (config("web", "contact") or
122 return (config("web", "contact") or
122 config("ui", "username") or
123 config("ui", "username") or
123 os.environ.get("EMAIL") or "")
124 os.environ.get("EMAIL") or "")
@@ -1,133 +1,134 b''
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import socket, cgi, errno
9 import socket, cgi, errno
10 from mercurial import util
10 from mercurial import util
11 from common import ErrorResponse, statusmessage
11 from common import ErrorResponse, statusmessage
12
12
13 shortcuts = {
13 shortcuts = {
14 'cl': [('cmd', ['changelog']), ('rev', None)],
14 'cl': [('cmd', ['changelog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
23 'tags': [('cmd', ['tags'])],
23 'tags': [('cmd', ['tags'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
25 'static': [('cmd', ['static']), ('file', None)]
25 'static': [('cmd', ['static']), ('file', None)]
26 }
26 }
27
27
28 def expand(form):
28 def expand(form):
29 for k in shortcuts.iterkeys():
29 for k in shortcuts.iterkeys():
30 if k in form:
30 if k in form:
31 for name, value in shortcuts[k]:
31 for name, value in shortcuts[k]:
32 if value is None:
32 if value is None:
33 value = form[k]
33 value = form[k]
34 form[name] = value
34 form[name] = value
35 del form[k]
35 del form[k]
36 return form
36 return form
37
37
38 class wsgirequest(object):
38 class wsgirequest(object):
39 def __init__(self, wsgienv, start_response):
39 def __init__(self, wsgienv, start_response):
40 version = wsgienv['wsgi.version']
40 version = wsgienv['wsgi.version']
41 if (version < (1, 0)) or (version >= (2, 0)):
41 if (version < (1, 0)) or (version >= (2, 0)):
42 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
42 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
43 % version)
43 % version)
44 self.inp = wsgienv['wsgi.input']
44 self.inp = wsgienv['wsgi.input']
45 self.err = wsgienv['wsgi.errors']
45 self.err = wsgienv['wsgi.errors']
46 self.threaded = wsgienv['wsgi.multithread']
46 self.threaded = wsgienv['wsgi.multithread']
47 self.multiprocess = wsgienv['wsgi.multiprocess']
47 self.multiprocess = wsgienv['wsgi.multiprocess']
48 self.run_once = wsgienv['wsgi.run_once']
48 self.run_once = wsgienv['wsgi.run_once']
49 self.env = wsgienv
49 self.env = wsgienv
50 self.form = expand(cgi.parse(self.inp, self.env, keep_blank_values=1))
50 self.form = expand(cgi.parse(self.inp, self.env, keep_blank_values=1))
51 self._start_response = start_response
51 self._start_response = start_response
52 self.server_write = None
52 self.server_write = None
53 self.headers = []
53 self.headers = []
54
54
55 def __iter__(self):
55 def __iter__(self):
56 return iter([])
56 return iter([])
57
57
58 def read(self, count=-1):
58 def read(self, count=-1):
59 return self.inp.read(count)
59 return self.inp.read(count)
60
60
61 def drain(self):
61 def drain(self):
62 '''need to read all data from request, httplib is half-duplex'''
62 '''need to read all data from request, httplib is half-duplex'''
63 length = int(self.env.get('CONTENT_LENGTH', 0))
63 length = int(self.env.get('CONTENT_LENGTH', 0))
64 for s in util.filechunkiter(self.inp, limit=length):
64 for s in util.filechunkiter(self.inp, limit=length):
65 pass
65 pass
66
66
67 def respond(self, status, type=None, filename=None, length=0):
67 def respond(self, status, type=None, filename=None, length=0):
68 if self._start_response is not None:
68 if self._start_response is not None:
69
69
70 self.httphdr(type, filename, length)
70 self.httphdr(type, filename, length)
71 if not self.headers:
71 if not self.headers:
72 raise RuntimeError("request.write called before headers sent")
72 raise RuntimeError("request.write called before headers sent")
73
73
74 for k, v in self.headers:
74 for k, v in self.headers:
75 if not isinstance(v, str):
75 if not isinstance(v, str):
76 raise TypeError('header value must be string: %r' % v)
76 raise TypeError('header value must be string: %r' % v)
77
77
78 if isinstance(status, ErrorResponse):
78 if isinstance(status, ErrorResponse):
79 status = statusmessage(status.code)
79 status = statusmessage(status.code)
80 self.header(status.headers)
80 elif status == 200:
81 elif status == 200:
81 status = '200 Script output follows'
82 status = '200 Script output follows'
82 elif isinstance(status, int):
83 elif isinstance(status, int):
83 status = statusmessage(status)
84 status = statusmessage(status)
84
85
85 self.server_write = self._start_response(status, self.headers)
86 self.server_write = self._start_response(status, self.headers)
86 self._start_response = None
87 self._start_response = None
87 self.headers = []
88 self.headers = []
88
89
89 def write(self, thing):
90 def write(self, thing):
90 if hasattr(thing, "__iter__"):
91 if hasattr(thing, "__iter__"):
91 for part in thing:
92 for part in thing:
92 self.write(part)
93 self.write(part)
93 else:
94 else:
94 thing = str(thing)
95 thing = str(thing)
95 try:
96 try:
96 self.server_write(thing)
97 self.server_write(thing)
97 except socket.error, inst:
98 except socket.error, inst:
98 if inst[0] != errno.ECONNRESET:
99 if inst[0] != errno.ECONNRESET:
99 raise
100 raise
100
101
101 def writelines(self, lines):
102 def writelines(self, lines):
102 for line in lines:
103 for line in lines:
103 self.write(line)
104 self.write(line)
104
105
105 def flush(self):
106 def flush(self):
106 return None
107 return None
107
108
108 def close(self):
109 def close(self):
109 return None
110 return None
110
111
111 def header(self, headers=[('Content-Type','text/html')]):
112 def header(self, headers=[('Content-Type','text/html')]):
112 self.headers.extend(headers)
113 self.headers.extend(headers)
113
114
114 def httphdr(self, type=None, filename=None, length=0, headers={}):
115 def httphdr(self, type=None, filename=None, length=0, headers={}):
115 headers = headers.items()
116 headers = headers.items()
116 if type is not None:
117 if type is not None:
117 headers.append(('Content-Type', type))
118 headers.append(('Content-Type', type))
118 if filename:
119 if filename:
119 filename = (filename.split('/')[-1]
120 filename = (filename.split('/')[-1]
120 .replace('\\', '\\\\').replace('"', '\\"'))
121 .replace('\\', '\\\\').replace('"', '\\"'))
121 headers.append(('Content-Disposition',
122 headers.append(('Content-Disposition',
122 'inline; filename="%s"' % filename))
123 'inline; filename="%s"' % filename))
123 if length:
124 if length:
124 headers.append(('Content-Length', str(length)))
125 headers.append(('Content-Length', str(length)))
125 self.header(headers)
126 self.header(headers)
126
127
127 def wsgiapplication(app_maker):
128 def wsgiapplication(app_maker):
128 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
129 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
129 can and should now be used as a WSGI application.'''
130 can and should now be used as a WSGI application.'''
130 application = app_maker()
131 application = app_maker()
131 def run_wsgi(env, respond):
132 def run_wsgi(env, respond):
132 return application(env, respond)
133 return application(env, respond)
133 return run_wsgi
134 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now