##// END OF EJS Templates
hgweb: give ErrorResponse a descriptive string/Exception representation...
Mads Kiilerich -
r13444:75f5f312 stable
parent child Browse files
Show More
@@ -1,164 +1,163 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 of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_NOT_MODIFIED = 304
12 HTTP_NOT_MODIFIED = 304
13 HTTP_BAD_REQUEST = 400
13 HTTP_BAD_REQUEST = 400
14 HTTP_UNAUTHORIZED = 401
14 HTTP_UNAUTHORIZED = 401
15 HTTP_FORBIDDEN = 403
15 HTTP_FORBIDDEN = 403
16 HTTP_NOT_FOUND = 404
16 HTTP_NOT_FOUND = 404
17 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_METHOD_NOT_ALLOWED = 405
18 HTTP_SERVER_ERROR = 500
18 HTTP_SERVER_ERROR = 500
19
19
20 # Hooks for hgweb permission checks; extensions can add hooks here. Each hook
20 # Hooks for hgweb permission checks; extensions can add hooks here. Each hook
21 # is invoked like this: hook(hgweb, request, operation), where operation is
21 # is invoked like this: hook(hgweb, request, operation), where operation is
22 # either read, pull or push. Hooks should either raise an ErrorResponse
22 # either read, pull or push. Hooks should either raise an ErrorResponse
23 # exception, or just return.
23 # exception, or just return.
24 # It is possible to do both authentication and authorization through this.
24 # It is possible to do both authentication and authorization through this.
25 permhooks = []
25 permhooks = []
26
26
27 def checkauthz(hgweb, req, op):
27 def checkauthz(hgweb, req, op):
28 '''Check permission for operation based on request data (including
28 '''Check permission for operation based on request data (including
29 authentication info). Return if op allowed, else raise an ErrorResponse
29 authentication info). Return if op allowed, else raise an ErrorResponse
30 exception.'''
30 exception.'''
31
31
32 user = req.env.get('REMOTE_USER')
32 user = req.env.get('REMOTE_USER')
33
33
34 deny_read = hgweb.configlist('web', 'deny_read')
34 deny_read = hgweb.configlist('web', 'deny_read')
35 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
35 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
36 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
36 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
37
37
38 allow_read = hgweb.configlist('web', 'allow_read')
38 allow_read = hgweb.configlist('web', 'allow_read')
39 result = (not allow_read) or (allow_read == ['*'])
39 result = (not allow_read) or (allow_read == ['*'])
40 if not (result or user in allow_read):
40 if not (result or user in allow_read):
41 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
41 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
42
42
43 if op == 'pull' and not hgweb.allowpull:
43 if op == 'pull' and not hgweb.allowpull:
44 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
44 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
45 elif op == 'pull' or op is None: # op is None for interface requests
45 elif op == 'pull' or op is None: # op is None for interface requests
46 return
46 return
47
47
48 # enforce that you can only push using POST requests
48 # enforce that you can only push using POST requests
49 if req.env['REQUEST_METHOD'] != 'POST':
49 if req.env['REQUEST_METHOD'] != 'POST':
50 msg = 'push requires POST request'
50 msg = 'push requires POST request'
51 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
51 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
52
52
53 # require ssl by default for pushing, auth info cannot be sniffed
53 # require ssl by default for pushing, auth info cannot be sniffed
54 # and replayed
54 # and replayed
55 scheme = req.env.get('wsgi.url_scheme')
55 scheme = req.env.get('wsgi.url_scheme')
56 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
56 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
57 raise ErrorResponse(HTTP_OK, 'ssl required')
57 raise ErrorResponse(HTTP_OK, 'ssl required')
58
58
59 deny = hgweb.configlist('web', 'deny_push')
59 deny = hgweb.configlist('web', 'deny_push')
60 if deny and (not user or deny == ['*'] or user in deny):
60 if deny and (not user or deny == ['*'] or user in deny):
61 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
61 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
62
62
63 allow = hgweb.configlist('web', 'allow_push')
63 allow = hgweb.configlist('web', 'allow_push')
64 result = allow and (allow == ['*'] or user in allow)
64 result = allow and (allow == ['*'] or user in allow)
65 if not result:
65 if not result:
66 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
66 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
67
67
68 # Add the default permhook, which provides simple authorization.
68 # Add the default permhook, which provides simple authorization.
69 permhooks.append(checkauthz)
69 permhooks.append(checkauthz)
70
70
71
71
72 class ErrorResponse(Exception):
72 class ErrorResponse(Exception):
73 def __init__(self, code, message=None, headers=[]):
73 def __init__(self, code, message=None, headers=[]):
74 Exception.__init__(self)
74 if message is None:
75 message = _statusmessage(code)
76 Exception.__init__(self, code, message)
75 self.code = code
77 self.code = code
78 self.message = message
76 self.headers = headers
79 self.headers = headers
77 if message is not None:
78 self.message = message
79 else:
80 self.message = _statusmessage(code)
81
80
82 def _statusmessage(code):
81 def _statusmessage(code):
83 from BaseHTTPServer import BaseHTTPRequestHandler
82 from BaseHTTPServer import BaseHTTPRequestHandler
84 responses = BaseHTTPRequestHandler.responses
83 responses = BaseHTTPRequestHandler.responses
85 return responses.get(code, ('Error', 'Unknown error'))[0]
84 return responses.get(code, ('Error', 'Unknown error'))[0]
86
85
87 def statusmessage(code, message=None):
86 def statusmessage(code, message=None):
88 return '%d %s' % (code, message or _statusmessage(code))
87 return '%d %s' % (code, message or _statusmessage(code))
89
88
90 def get_mtime(spath):
89 def get_mtime(spath):
91 cl_path = os.path.join(spath, "00changelog.i")
90 cl_path = os.path.join(spath, "00changelog.i")
92 if os.path.exists(cl_path):
91 if os.path.exists(cl_path):
93 return os.stat(cl_path).st_mtime
92 return os.stat(cl_path).st_mtime
94 else:
93 else:
95 return os.stat(spath).st_mtime
94 return os.stat(spath).st_mtime
96
95
97 def staticfile(directory, fname, req):
96 def staticfile(directory, fname, req):
98 """return a file inside directory with guessed Content-Type header
97 """return a file inside directory with guessed Content-Type header
99
98
100 fname always uses '/' as directory separator and isn't allowed to
99 fname always uses '/' as directory separator and isn't allowed to
101 contain unusual path components.
100 contain unusual path components.
102 Content-Type is guessed using the mimetypes module.
101 Content-Type is guessed using the mimetypes module.
103 Return an empty string if fname is illegal or file not found.
102 Return an empty string if fname is illegal or file not found.
104
103
105 """
104 """
106 parts = fname.split('/')
105 parts = fname.split('/')
107 for part in parts:
106 for part in parts:
108 if (part in ('', os.curdir, os.pardir) or
107 if (part in ('', os.curdir, os.pardir) or
109 os.sep in part or os.altsep is not None and os.altsep in part):
108 os.sep in part or os.altsep is not None and os.altsep in part):
110 return ""
109 return ""
111 fpath = os.path.join(*parts)
110 fpath = os.path.join(*parts)
112 if isinstance(directory, str):
111 if isinstance(directory, str):
113 directory = [directory]
112 directory = [directory]
114 for d in directory:
113 for d in directory:
115 path = os.path.join(d, fpath)
114 path = os.path.join(d, fpath)
116 if os.path.exists(path):
115 if os.path.exists(path):
117 break
116 break
118 try:
117 try:
119 os.stat(path)
118 os.stat(path)
120 ct = mimetypes.guess_type(path)[0] or "text/plain"
119 ct = mimetypes.guess_type(path)[0] or "text/plain"
121 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
120 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
122 fp = open(path, 'rb')
121 fp = open(path, 'rb')
123 data = fp.read()
122 data = fp.read()
124 fp.close()
123 fp.close()
125 return data
124 return data
126 except TypeError:
125 except TypeError:
127 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
126 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
128 except OSError, err:
127 except OSError, err:
129 if err.errno == errno.ENOENT:
128 if err.errno == errno.ENOENT:
130 raise ErrorResponse(HTTP_NOT_FOUND)
129 raise ErrorResponse(HTTP_NOT_FOUND)
131 else:
130 else:
132 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
131 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
133
132
134 def paritygen(stripecount, offset=0):
133 def paritygen(stripecount, offset=0):
135 """count parity of horizontal stripes for easier reading"""
134 """count parity of horizontal stripes for easier reading"""
136 if stripecount and offset:
135 if stripecount and offset:
137 # account for offset, e.g. due to building the list in reverse
136 # account for offset, e.g. due to building the list in reverse
138 count = (stripecount + offset) % stripecount
137 count = (stripecount + offset) % stripecount
139 parity = (stripecount + offset) / stripecount & 1
138 parity = (stripecount + offset) / stripecount & 1
140 else:
139 else:
141 count = 0
140 count = 0
142 parity = 0
141 parity = 0
143 while True:
142 while True:
144 yield parity
143 yield parity
145 count += 1
144 count += 1
146 if stripecount and count >= stripecount:
145 if stripecount and count >= stripecount:
147 parity = 1 - parity
146 parity = 1 - parity
148 count = 0
147 count = 0
149
148
150 def get_contact(config):
149 def get_contact(config):
151 """Return repo contact information or empty string.
150 """Return repo contact information or empty string.
152
151
153 web.contact is the primary source, but if that is not set, try
152 web.contact is the primary source, but if that is not set, try
154 ui.username or $EMAIL as a fallback to display something useful.
153 ui.username or $EMAIL as a fallback to display something useful.
155 """
154 """
156 return (config("web", "contact") or
155 return (config("web", "contact") or
157 config("ui", "username") or
156 config("ui", "username") or
158 os.environ.get("EMAIL") or "")
157 os.environ.get("EMAIL") or "")
159
158
160 def caching(web, req):
159 def caching(web, req):
161 tag = str(web.mtime)
160 tag = str(web.mtime)
162 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
161 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
163 raise ErrorResponse(HTTP_NOT_MODIFIED)
162 raise ErrorResponse(HTTP_NOT_MODIFIED)
164 req.headers.append(('ETag', tag))
163 req.headers.append(('ETag', tag))
General Comments 0
You need to be logged in to leave comments. Login now