##// END OF EJS Templates
server: ensure the incoming request falls under the prefix value...
Matt Harbison -
r37508:7de7bd40 4.5.3 stable
parent child Browse files
Show More
@@ -1,347 +1,355
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import os
13 13 import socket
14 14 import sys
15 15 import traceback
16 16
17 17 from ..i18n import _
18 18
19 19 from .. import (
20 20 encoding,
21 21 error,
22 22 pycompat,
23 23 util,
24 24 )
25 25
26 26 httpservermod = util.httpserver
27 27 socketserver = util.socketserver
28 28 urlerr = util.urlerr
29 29 urlreq = util.urlreq
30 30
31 31 from . import (
32 32 common,
33 33 )
34 34
35 35 def _splitURI(uri):
36 36 """Return path and query that has been split from uri
37 37
38 38 Just like CGI environment, the path is unquoted, the query is
39 39 not.
40 40 """
41 41 if r'?' in uri:
42 42 path, query = uri.split(r'?', 1)
43 43 else:
44 44 path, query = uri, r''
45 45 return urlreq.unquote(path), query
46 46
47 47 class _error_logger(object):
48 48 def __init__(self, handler):
49 49 self.handler = handler
50 50 def flush(self):
51 51 pass
52 52 def write(self, str):
53 53 self.writelines(str.split('\n'))
54 54 def writelines(self, seq):
55 55 for msg in seq:
56 56 self.handler.log_error("HG error: %s", msg)
57 57
58 58 class _httprequesthandler(httpservermod.basehttprequesthandler):
59 59
60 60 url_scheme = 'http'
61 61
62 62 @staticmethod
63 63 def preparehttpserver(httpserver, ui):
64 64 """Prepare .socket of new HTTPServer instance"""
65 65
66 66 def __init__(self, *args, **kargs):
67 67 self.protocol_version = r'HTTP/1.1'
68 68 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
69 69
70 70 def _log_any(self, fp, format, *args):
71 71 fp.write(pycompat.sysbytes(
72 72 r"%s - - [%s] %s" % (self.client_address[0],
73 73 self.log_date_time_string(),
74 74 format % args)) + '\n')
75 75 fp.flush()
76 76
77 77 def log_error(self, format, *args):
78 78 self._log_any(self.server.errorlog, format, *args)
79 79
80 80 def log_message(self, format, *args):
81 81 self._log_any(self.server.accesslog, format, *args)
82 82
83 83 def log_request(self, code=r'-', size=r'-'):
84 84 xheaders = []
85 85 if util.safehasattr(self, 'headers'):
86 86 xheaders = [h for h in self.headers.items()
87 87 if h[0].startswith(r'x-')]
88 88 self.log_message(r'"%s" %s %s%s',
89 89 self.requestline, str(code), str(size),
90 90 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
91 91
92 92 def do_write(self):
93 93 try:
94 94 self.do_hgweb()
95 95 except socket.error as inst:
96 96 if inst[0] != errno.EPIPE:
97 97 raise
98 98
99 99 def do_POST(self):
100 100 try:
101 101 self.do_write()
102 102 except Exception:
103 103 self._start_response("500 Internal Server Error", [])
104 104 self._write("Internal Server Error")
105 105 self._done()
106 106 tb = r"".join(traceback.format_exception(*sys.exc_info()))
107 107 # We need a native-string newline to poke in the log
108 108 # message, because we won't get a newline when using an
109 109 # r-string. This is the easy way out.
110 110 newline = chr(10)
111 111 self.log_error(r"Exception happened during processing "
112 112 r"request '%s':%s%s", self.path, newline, tb)
113 113
114 114 def do_GET(self):
115 115 self.do_POST()
116 116
117 117 def do_hgweb(self):
118 118 self.sent_headers = False
119 119 path, query = _splitURI(self.path)
120 120
121 # Ensure the slicing of path below is valid
122 if (path != self.server.prefix
123 and not path.startswith(self.server.prefix + b'/')):
124 self._start_response(common.statusmessage(404), [])
125 self._write("Not Found")
126 self._done()
127 return
128
121 129 env = {}
122 130 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
123 131 env[r'REQUEST_METHOD'] = self.command
124 132 env[r'SERVER_NAME'] = self.server.server_name
125 133 env[r'SERVER_PORT'] = str(self.server.server_port)
126 134 env[r'REQUEST_URI'] = self.path
127 135 env[r'SCRIPT_NAME'] = self.server.prefix
128 136 env[r'PATH_INFO'] = path[len(self.server.prefix):]
129 137 env[r'REMOTE_HOST'] = self.client_address[0]
130 138 env[r'REMOTE_ADDR'] = self.client_address[0]
131 139 if query:
132 140 env[r'QUERY_STRING'] = query
133 141
134 142 if pycompat.ispy3:
135 143 if self.headers.get_content_type() is None:
136 144 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
137 145 else:
138 146 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
139 147 length = self.headers.get('content-length')
140 148 else:
141 149 if self.headers.typeheader is None:
142 150 env[r'CONTENT_TYPE'] = self.headers.type
143 151 else:
144 152 env[r'CONTENT_TYPE'] = self.headers.typeheader
145 153 length = self.headers.getheader('content-length')
146 154 if length:
147 155 env[r'CONTENT_LENGTH'] = length
148 156 for header in [h for h in self.headers.keys()
149 157 if h not in ('content-type', 'content-length')]:
150 158 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
151 159 hval = self.headers.get(header)
152 160 hval = hval.replace(r'\n', r'').strip()
153 161 if hval:
154 162 env[hkey] = hval
155 163 env[r'SERVER_PROTOCOL'] = self.request_version
156 164 env[r'wsgi.version'] = (1, 0)
157 165 env[r'wsgi.url_scheme'] = self.url_scheme
158 166 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
159 167 self.rfile = common.continuereader(self.rfile, self.wfile.write)
160 168
161 169 env[r'wsgi.input'] = self.rfile
162 170 env[r'wsgi.errors'] = _error_logger(self)
163 171 env[r'wsgi.multithread'] = isinstance(self.server,
164 172 socketserver.ThreadingMixIn)
165 173 env[r'wsgi.multiprocess'] = isinstance(self.server,
166 174 socketserver.ForkingMixIn)
167 175 env[r'wsgi.run_once'] = 0
168 176
169 177 self.saved_status = None
170 178 self.saved_headers = []
171 179 self.length = None
172 180 self._chunked = None
173 181 for chunk in self.server.application(env, self._start_response):
174 182 self._write(chunk)
175 183 if not self.sent_headers:
176 184 self.send_headers()
177 185 self._done()
178 186
179 187 def send_headers(self):
180 188 if not self.saved_status:
181 189 raise AssertionError("Sending headers before "
182 190 "start_response() called")
183 191 saved_status = self.saved_status.split(None, 1)
184 192 saved_status[0] = int(saved_status[0])
185 193 self.send_response(*saved_status)
186 194 self.length = None
187 195 self._chunked = False
188 196 for h in self.saved_headers:
189 197 self.send_header(*h)
190 198 if h[0].lower() == 'content-length':
191 199 self.length = int(h[1])
192 200 if (self.length is None and
193 201 saved_status[0] != common.HTTP_NOT_MODIFIED):
194 202 self._chunked = (not self.close_connection and
195 203 self.request_version == "HTTP/1.1")
196 204 if self._chunked:
197 205 self.send_header(r'Transfer-Encoding', r'chunked')
198 206 else:
199 207 self.send_header(r'Connection', r'close')
200 208 self.end_headers()
201 209 self.sent_headers = True
202 210
203 211 def _start_response(self, http_status, headers, exc_info=None):
204 212 code, msg = http_status.split(None, 1)
205 213 code = int(code)
206 214 self.saved_status = http_status
207 215 bad_headers = ('connection', 'transfer-encoding')
208 216 self.saved_headers = [h for h in headers
209 217 if h[0].lower() not in bad_headers]
210 218 return self._write
211 219
212 220 def _write(self, data):
213 221 if not self.saved_status:
214 222 raise AssertionError("data written before start_response() called")
215 223 elif not self.sent_headers:
216 224 self.send_headers()
217 225 if self.length is not None:
218 226 if len(data) > self.length:
219 227 raise AssertionError("Content-length header sent, but more "
220 228 "bytes than specified are being written.")
221 229 self.length = self.length - len(data)
222 230 elif self._chunked and data:
223 231 data = '%x\r\n%s\r\n' % (len(data), data)
224 232 self.wfile.write(data)
225 233 self.wfile.flush()
226 234
227 235 def _done(self):
228 236 if self._chunked:
229 237 self.wfile.write('0\r\n\r\n')
230 238 self.wfile.flush()
231 239
232 240 class _httprequesthandlerssl(_httprequesthandler):
233 241 """HTTPS handler based on Python's ssl module"""
234 242
235 243 url_scheme = 'https'
236 244
237 245 @staticmethod
238 246 def preparehttpserver(httpserver, ui):
239 247 try:
240 248 from .. import sslutil
241 249 sslutil.modernssl
242 250 except ImportError:
243 251 raise error.Abort(_("SSL support is unavailable"))
244 252
245 253 certfile = ui.config('web', 'certificate')
246 254
247 255 # These config options are currently only meant for testing. Use
248 256 # at your own risk.
249 257 cafile = ui.config('devel', 'servercafile')
250 258 reqcert = ui.configbool('devel', 'serverrequirecert')
251 259
252 260 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
253 261 ui,
254 262 certfile=certfile,
255 263 cafile=cafile,
256 264 requireclientcert=reqcert)
257 265
258 266 def setup(self):
259 267 self.connection = self.request
260 268 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
261 269 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
262 270
263 271 try:
264 272 import threading
265 273 threading.activeCount() # silence pyflakes and bypass demandimport
266 274 _mixin = socketserver.ThreadingMixIn
267 275 except ImportError:
268 276 if util.safehasattr(os, "fork"):
269 277 _mixin = socketserver.ForkingMixIn
270 278 else:
271 279 class _mixin(object):
272 280 pass
273 281
274 282 def openlog(opt, default):
275 283 if opt and opt != '-':
276 284 return open(opt, 'a')
277 285 return default
278 286
279 287 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
280 288
281 289 # SO_REUSEADDR has broken semantics on windows
282 290 if pycompat.iswindows:
283 291 allow_reuse_address = 0
284 292
285 293 def __init__(self, ui, app, addr, handler, **kwargs):
286 294 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
287 295 self.daemon_threads = True
288 296 self.application = app
289 297
290 298 handler.preparehttpserver(self, ui)
291 299
292 300 prefix = ui.config('web', 'prefix')
293 301 if prefix:
294 302 prefix = '/' + prefix.strip('/')
295 303 self.prefix = prefix
296 304
297 305 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
298 306 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
299 307 self.accesslog = alog
300 308 self.errorlog = elog
301 309
302 310 self.addr, self.port = self.socket.getsockname()[0:2]
303 311 self.fqaddr = socket.getfqdn(addr[0])
304 312
305 313 class IPv6HTTPServer(MercurialHTTPServer):
306 314 address_family = getattr(socket, 'AF_INET6', None)
307 315 def __init__(self, *args, **kwargs):
308 316 if self.address_family is None:
309 317 raise error.RepoError(_('IPv6 is not available on this system'))
310 318 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
311 319
312 320 def create_server(ui, app):
313 321
314 322 if ui.config('web', 'certificate'):
315 323 handler = _httprequesthandlerssl
316 324 else:
317 325 handler = _httprequesthandler
318 326
319 327 if ui.configbool('web', 'ipv6'):
320 328 cls = IPv6HTTPServer
321 329 else:
322 330 cls = MercurialHTTPServer
323 331
324 332 # ugly hack due to python issue5853 (for threaded use)
325 333 try:
326 334 import mimetypes
327 335 mimetypes.init()
328 336 except UnicodeDecodeError:
329 337 # Python 2.x's mimetypes module attempts to decode strings
330 338 # from Windows' ANSI APIs as ascii (fail), then re-encode them
331 339 # as ascii (clown fail), because the default Python Unicode
332 340 # codec is hardcoded as ascii.
333 341
334 342 sys.argv # unwrap demand-loader so that reload() works
335 343 reload(sys) # resurrect sys.setdefaultencoding()
336 344 oldenc = sys.getdefaultencoding()
337 345 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
338 346 mimetypes.init()
339 347 sys.setdefaultencoding(oldenc)
340 348
341 349 address = ui.config('web', 'address')
342 350 port = util.getport(ui.config('web', 'port'))
343 351 try:
344 352 return cls(ui, app, (address, port), handler)
345 353 except socket.error as inst:
346 354 raise error.Abort(_("cannot start server at '%s:%d': %s")
347 355 % (address, port, encoding.strtolocal(inst.args[1])))
@@ -1,81 +1,101
1 1 #require serve
2 2
3 3 $ hgserve()
4 4 > {
5 5 > hg serve -a localhost -d --pid-file=hg.pid -E errors.log -v $@ \
6 6 > | sed -e "s/:$HGPORT1\\([^0-9]\\)/:HGPORT1\1/g" \
7 7 > -e "s/:$HGPORT2\\([^0-9]\\)/:HGPORT2\1/g" \
8 8 > -e 's/http:\/\/[^/]*\//http:\/\/localhost\//'
9 9 > cat hg.pid >> "$DAEMON_PIDS"
10 10 > echo % errors
11 11 > cat errors.log
12 12 > killdaemons.py hg.pid
13 13 > }
14 14
15 15 $ hg init test
16 16 $ cd test
17 17 $ echo '[web]' > .hg/hgrc
18 18 $ echo 'accesslog = access.log' >> .hg/hgrc
19 19 $ echo "port = $HGPORT1" >> .hg/hgrc
20 20
21 21 Without -v
22 22
23 23 $ hg serve -a localhost -p $HGPORT -d --pid-file=hg.pid -E errors.log
24 24 $ cat hg.pid >> "$DAEMON_PIDS"
25 25 $ if [ -f access.log ]; then
26 26 > echo 'access log created - .hg/hgrc respected'
27 27 > fi
28 28 access log created - .hg/hgrc respected
29 29
30 30 errors
31 31
32 32 $ cat errors.log
33 33
34 34 With -v
35 35
36 36 $ hgserve
37 37 listening at http://localhost/ (bound to *$LOCALIP*:HGPORT1) (glob) (?)
38 38 % errors
39 39
40 40 With -v and -p HGPORT2
41 41
42 42 $ hgserve -p "$HGPORT2"
43 43 listening at http://localhost/ (bound to *$LOCALIP*:HGPORT2) (glob) (?)
44 44 % errors
45 45
46 46 With -v and -p daytime (should fail because low port)
47 47
48 48 #if no-root no-windows
49 49 $ KILLQUIETLY=Y
50 50 $ hgserve -p daytime
51 51 abort: cannot start server at 'localhost:13': Permission denied
52 52 abort: child process failed to start
53 53 % errors
54 54 $ KILLQUIETLY=N
55 55 #endif
56 56
57 57 With --prefix foo
58 58
59 59 $ hgserve --prefix foo
60 60 listening at http://localhost/foo/ (bound to *$LOCALIP*:HGPORT1) (glob) (?)
61 61 % errors
62 62
63 63 With --prefix /foo
64 64
65 65 $ hgserve --prefix /foo
66 66 listening at http://localhost/foo/ (bound to *$LOCALIP*:HGPORT1) (glob) (?)
67 67 % errors
68 68
69 69 With --prefix foo/
70 70
71 71 $ hgserve --prefix foo/
72 72 listening at http://localhost/foo/ (bound to *$LOCALIP*:HGPORT1) (glob) (?)
73 73 % errors
74 74
75 75 With --prefix /foo/
76 76
77 77 $ hgserve --prefix /foo/
78 78 listening at http://localhost/foo/ (bound to *$LOCALIP*:HGPORT1) (glob) (?)
79 79 % errors
80 80
81 $ $PYTHON $RUNTESTDIR/killdaemons.py $DAEMON_PIDS
82
83 With out of bounds accesses
84
85 $ rm access.log
86 $ hg serve -a localhost -p $HGPORT -d --prefix some/dir \
87 > --pid-file=hg.pid -E errors.log
88 $ cat hg.pid >> "$DAEMON_PIDS"
89
90 $ hg id http://localhost:$HGPORT/some/dir7
91 abort: HTTP Error 404: Not Found
92 [255]
93 $ hg id http://localhost:$HGPORT/some
94 abort: HTTP Error 404: Not Found
95 [255]
96
97 $ cat access.log errors.log
98 $LOCALIP - - [$LOGDATE$] "GET /some/dir7?cmd=capabilities HTTP/1.1" 404 - (glob)
99 $LOCALIP - - [$LOGDATE$] "GET /some?cmd=capabilities HTTP/1.1" 404 - (glob)
100
81 101 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now