121
|
1 # hgweb/server.py - The standalone hg web server.
|
|
2 #
|
|
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
|
|
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
|
|
5 #
|
|
6 # This software may be used and distributed according to the terms of the
|
|
7 # GNU General Public License version 2, incorporated herein by reference.
|
|
8
|
|
9 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
|
|
10 from mercurial import hg, util, error
|
|
11 from hgweb_mod import hgweb
|
|
12 from hgwebdir_mod import hgwebdir
|
|
13 from mercurial.i18n import _
|
|
14
|
|
15 def _splitURI(uri):
|
|
16 """ Return path and query splited from uri
|
|
17
|
|
18 Just like CGI environment, the path is unquoted, the query is
|
|
19 not.
|
|
20 """
|
|
21 if '?' in uri:
|
|
22 path, query = uri.split('?', 1)
|
|
23 else:
|
|
24 path, query = uri, ''
|
|
25 return urllib.unquote(path), query
|
|
26
|
|
27 class _error_logger(object):
|
|
28 def __init__(self, handler):
|
|
29 self.handler = handler
|
|
30 def flush(self):
|
|
31 pass
|
|
32 def write(self, str):
|
|
33 self.writelines(str.split('\n'))
|
|
34 def writelines(self, seq):
|
|
35 for msg in seq:
|
|
36 self.handler.log_error("HG error: %s", msg)
|
|
37
|
|
38 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
|
|
39
|
|
40 url_scheme = 'http'
|
|
41
|
|
42 def __init__(self, *args, **kargs):
|
|
43 self.protocol_version = 'HTTP/1.1'
|
|
44 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
|
|
45
|
|
46 def _log_any(self, fp, format, *args):
|
|
47 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
|
|
48 self.log_date_time_string(),
|
|
49 format % args))
|
|
50 fp.flush()
|
|
51
|
|
52 def log_error(self, format, *args):
|
|
53 self._log_any(self.server.errorlog, format, *args)
|
|
54
|
|
55 def log_message(self, format, *args):
|
|
56 self._log_any(self.server.accesslog, format, *args)
|
|
57
|
|
58 def do_write(self):
|
|
59 try:
|
|
60 self.do_hgweb()
|
|
61 except socket.error, inst:
|
|
62 if inst[0] != errno.EPIPE:
|
|
63 raise
|
|
64
|
|
65 def do_POST(self):
|
|
66 try:
|
|
67 self.do_write()
|
|
68 except StandardError:
|
|
69 self._start_response("500 Internal Server Error", [])
|
|
70 self._write("Internal Server Error")
|
|
71 tb = "".join(traceback.format_exception(*sys.exc_info()))
|
|
72 self.log_error("Exception happened during processing "
|
|
73 "request '%s':\n%s", self.path, tb)
|
|
74
|
|
75 def do_GET(self):
|
|
76 self.do_POST()
|
|
77
|
|
78 def do_hgweb(self):
|
|
79 path, query = _splitURI(self.path)
|
|
80
|
|
81 env = {}
|
|
82 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
|
|
83 env['REQUEST_METHOD'] = self.command
|
|
84 env['SERVER_NAME'] = self.server.server_name
|
|
85 env['SERVER_PORT'] = str(self.server.server_port)
|
|
86 env['REQUEST_URI'] = self.path
|
|
87 env['SCRIPT_NAME'] = self.server.prefix
|
|
88 env['PATH_INFO'] = path[len(self.server.prefix):]
|
|
89 env['REMOTE_HOST'] = self.client_address[0]
|
|
90 env['REMOTE_ADDR'] = self.client_address[0]
|
|
91 if query:
|
|
92 env['QUERY_STRING'] = query
|
|
93
|
|
94 if self.headers.typeheader is None:
|
|
95 env['CONTENT_TYPE'] = self.headers.type
|
|
96 else:
|
|
97 env['CONTENT_TYPE'] = self.headers.typeheader
|
|
98 length = self.headers.getheader('content-length')
|
|
99 if length:
|
|
100 env['CONTENT_LENGTH'] = length
|
|
101 for header in [h for h in self.headers.keys()
|
|
102 if h not in ('content-type', 'content-length')]:
|
|
103 hkey = 'HTTP_' + header.replace('-', '_').upper()
|
|
104 hval = self.headers.getheader(header)
|
|
105 hval = hval.replace('\n', '').strip()
|
|
106 if hval:
|
|
107 env[hkey] = hval
|
|
108 env['SERVER_PROTOCOL'] = self.request_version
|
|
109 env['wsgi.version'] = (1, 0)
|
|
110 env['wsgi.url_scheme'] = self.url_scheme
|
|
111 env['wsgi.input'] = self.rfile
|
|
112 env['wsgi.errors'] = _error_logger(self)
|
|
113 env['wsgi.multithread'] = isinstance(self.server,
|
|
114 SocketServer.ThreadingMixIn)
|
|
115 env['wsgi.multiprocess'] = isinstance(self.server,
|
|
116 SocketServer.ForkingMixIn)
|
|
117 env['wsgi.run_once'] = 0
|
|
118
|
|
119 self.close_connection = True
|
|
120 self.saved_status = None
|
|
121 self.saved_headers = []
|
|
122 self.sent_headers = False
|
|
123 self.length = None
|
|
124 for chunk in self.server.application(env, self._start_response):
|
|
125 self._write(chunk)
|
|
126
|
|
127 def send_headers(self):
|
|
128 if not self.saved_status:
|
|
129 raise AssertionError("Sending headers before "
|
|
130 "start_response() called")
|
|
131 saved_status = self.saved_status.split(None, 1)
|
|
132 saved_status[0] = int(saved_status[0])
|
|
133 self.send_response(*saved_status)
|
|
134 should_close = True
|
|
135 for h in self.saved_headers:
|
|
136 self.send_header(*h)
|
|
137 if h[0].lower() == 'content-length':
|
|
138 should_close = False
|
|
139 self.length = int(h[1])
|
|
140 # The value of the Connection header is a list of case-insensitive
|
|
141 # tokens separated by commas and optional whitespace.
|
|
142 if 'close' in [token.strip().lower() for token in
|
|
143 self.headers.get('connection', '').split(',')]:
|
|
144 should_close = True
|
|
145 if should_close:
|
|
146 self.send_header('Connection', 'close')
|
|
147 self.close_connection = should_close
|
|
148 self.end_headers()
|
|
149 self.sent_headers = True
|
|
150
|
|
151 def _start_response(self, http_status, headers, exc_info=None):
|
|
152 code, msg = http_status.split(None, 1)
|
|
153 code = int(code)
|
|
154 self.saved_status = http_status
|
|
155 bad_headers = ('connection', 'transfer-encoding')
|
|
156 self.saved_headers = [h for h in headers
|
|
157 if h[0].lower() not in bad_headers]
|
|
158 return self._write
|
|
159
|
|
160 def _write(self, data):
|
|
161 if not self.saved_status:
|
|
162 raise AssertionError("data written before start_response() called")
|
|
163 elif not self.sent_headers:
|
|
164 self.send_headers()
|
|
165 if self.length is not None:
|
|
166 if len(data) > self.length:
|
|
167 raise AssertionError("Content-length header sent, but more "
|
|
168 "bytes than specified are being written.")
|
|
169 self.length = self.length - len(data)
|
|
170 self.wfile.write(data)
|
|
171 self.wfile.flush()
|
|
172
|
|
173 class _shgwebhandler(_hgwebhandler):
|
|
174
|
|
175 url_scheme = 'https'
|
|
176
|
|
177 def setup(self):
|
|
178 self.connection = self.request
|
|
179 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
|
|
180 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
|
|
181
|
|
182 def do_write(self):
|
|
183 from OpenSSL.SSL import SysCallError
|
|
184 try:
|
|
185 super(_shgwebhandler, self).do_write()
|
|
186 except SysCallError, inst:
|
|
187 if inst.args[0] != errno.EPIPE:
|
|
188 raise
|
|
189
|
|
190 def handle_one_request(self):
|
|
191 from OpenSSL.SSL import SysCallError, ZeroReturnError
|
|
192 try:
|
|
193 super(_shgwebhandler, self).handle_one_request()
|
|
194 except (SysCallError, ZeroReturnError):
|
|
195 self.close_connection = True
|
|
196 pass
|
|
197
|
|
198 def create_server(ui, repo):
|
|
199 use_threads = True
|
|
200
|
|
201 def openlog(opt, default):
|
|
202 if opt and opt != '-':
|
|
203 return open(opt, 'a')
|
|
204 return default
|
|
205
|
|
206 if repo is None:
|
|
207 myui = ui
|
|
208 else:
|
|
209 myui = repo.ui
|
|
210 address = myui.config("web", "address", "")
|
|
211 port = int(myui.config("web", "port", 8000))
|
|
212 prefix = myui.config("web", "prefix", "")
|
|
213 if prefix:
|
|
214 prefix = "/" + prefix.strip("/")
|
|
215 use_ipv6 = myui.configbool("web", "ipv6")
|
|
216 webdir_conf = myui.config("web", "webdir_conf")
|
|
217 ssl_cert = myui.config("web", "certificate")
|
|
218 accesslog = openlog(myui.config("web", "accesslog", "-"), sys.stdout)
|
|
219 errorlog = openlog(myui.config("web", "errorlog", "-"), sys.stderr)
|
|
220
|
|
221 if use_threads:
|
|
222 try:
|
|
223 from threading import activeCount
|
|
224 except ImportError:
|
|
225 use_threads = False
|
|
226
|
|
227 if use_threads:
|
|
228 _mixin = SocketServer.ThreadingMixIn
|
|
229 else:
|
|
230 if hasattr(os, "fork"):
|
|
231 _mixin = SocketServer.ForkingMixIn
|
|
232 else:
|
|
233 class _mixin:
|
|
234 pass
|
|
235
|
|
236 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
|
|
237
|
|
238 # SO_REUSEADDR has broken semantics on windows
|
|
239 if os.name == 'nt':
|
|
240 allow_reuse_address = 0
|
|
241
|
|
242 def __init__(self, *args, **kargs):
|
|
243 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
|
|
244 self.accesslog = accesslog
|
|
245 self.errorlog = errorlog
|
|
246 self.daemon_threads = True
|
|
247 def make_handler():
|
|
248 if webdir_conf:
|
|
249 hgwebobj = hgwebdir(webdir_conf, ui)
|
|
250 elif repo is not None:
|
|
251 hgwebobj = hgweb(hg.repository(repo.ui, repo.root))
|
|
252 else:
|
|
253 raise error.RepoError(_("There is no Mercurial repository"
|
|
254 " here (.hg not found)"))
|
|
255 return hgwebobj
|
|
256 self.application = make_handler()
|
|
257
|
|
258 if ssl_cert:
|
|
259 try:
|
|
260 from OpenSSL import SSL
|
|
261 ctx = SSL.Context(SSL.SSLv23_METHOD)
|
|
262 except ImportError:
|
|
263 raise util.Abort(_("SSL support is unavailable"))
|
|
264 ctx.use_privatekey_file(ssl_cert)
|
|
265 ctx.use_certificate_file(ssl_cert)
|
|
266 sock = socket.socket(self.address_family, self.socket_type)
|
|
267 self.socket = SSL.Connection(ctx, sock)
|
|
268 self.server_bind()
|
|
269 self.server_activate()
|
|
270
|
|
271 self.addr, self.port = self.socket.getsockname()[0:2]
|
|
272 self.prefix = prefix
|
|
273 self.fqaddr = socket.getfqdn(address)
|
|
274
|
|
275 class IPv6HTTPServer(MercurialHTTPServer):
|
|
276 address_family = getattr(socket, 'AF_INET6', None)
|
|
277
|
|
278 def __init__(self, *args, **kwargs):
|
|
279 if self.address_family is None:
|
|
280 raise error.RepoError(_('IPv6 is not available on this system'))
|
|
281 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
|
|
282
|
|
283 if ssl_cert:
|
|
284 handler = _shgwebhandler
|
|
285 else:
|
|
286 handler = _hgwebhandler
|
|
287
|
|
288 # ugly hack due to python issue5853 (for threaded use)
|
|
289 import mimetypes; mimetypes.init()
|
|
290
|
|
291 try:
|
|
292 if use_ipv6:
|
|
293 return IPv6HTTPServer((address, port), handler)
|
|
294 else:
|
|
295 return MercurialHTTPServer((address, port), handler)
|
|
296 except socket.error, inst:
|
|
297 raise util.Abort(_("cannot start server at '%s:%d': %s")
|
|
298 % (address, port, inst.args[1]))
|