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