server.py
298 lines
| 10.5 KiB
| text/x-python
|
PythonLexer
Eric Hopper
|
r2391 | # hgweb/server.py - The standalone hg web server. | ||
Eric Hopper
|
r2355 | # | ||
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||||
Thomas Arendsen Hein
|
r4635 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||
Eric Hopper
|
r2355 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
# GNU General Public License version 2, incorporated herein by reference. | ||||
Eric Hopper
|
r2355 | |||
Thomas Arendsen Hein
|
r4016 | import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback | ||
Matt Mackall
|
r7637 | from mercurial import hg, util, error | ||
Matt Mackall
|
r3877 | from hgweb_mod import hgweb | ||
from hgwebdir_mod import hgwebdir | ||||
Martin Geisler
|
r7225 | from mercurial.i18n import _ | ||
Eric Hopper
|
r2355 | |||
def _splitURI(uri): | ||||
""" Return path and query splited from uri | ||||
Just like CGI environment, the path is unquoted, the query is | ||||
not. | ||||
""" | ||||
if '?' in uri: | ||||
path, query = uri.split('?', 1) | ||||
else: | ||||
path, query = uri, '' | ||||
return urllib.unquote(path), query | ||||
Eric Hopper
|
r2506 | class _error_logger(object): | ||
def __init__(self, handler): | ||||
self.handler = handler | ||||
def flush(self): | ||||
pass | ||||
Benoit Boissinot
|
r3130 | def write(self, str): | ||
Eric Hopper
|
r2506 | self.writelines(str.split('\n')) | ||
Benoit Boissinot
|
r3130 | def writelines(self, seq): | ||
Eric Hopper
|
r2506 | for msg in seq: | ||
self.handler.log_error("HG error: %s", msg) | ||||
Eric Hopper
|
r2355 | class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler): | ||
Wesley J. Landaker
|
r4870 | |||
url_scheme = 'http' | ||||
Thomas Arendsen Hein
|
r4957 | |||
Eric Hopper
|
r2355 | def __init__(self, *args, **kargs): | ||
Vadim Gelfer
|
r2434 | self.protocol_version = 'HTTP/1.1' | ||
Eric Hopper
|
r2355 | BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs) | ||
Patrick Mezard
|
r5549 | def _log_any(self, fp, format, *args): | ||
fp.write("%s - - [%s] %s\n" % (self.client_address[0], | ||||
self.log_date_time_string(), | ||||
format % args)) | ||||
fp.flush() | ||||
Eric Hopper
|
r2355 | def log_error(self, format, *args): | ||
Patrick Mezard
|
r5549 | self._log_any(self.server.errorlog, format, *args) | ||
Eric Hopper
|
r2355 | |||
def log_message(self, format, *args): | ||||
Patrick Mezard
|
r5549 | self._log_any(self.server.accesslog, format, *args) | ||
Eric Hopper
|
r2355 | |||
Brendan Cully
|
r4860 | def do_write(self): | ||
try: | ||||
self.do_hgweb() | ||||
except socket.error, inst: | ||||
if inst[0] != errno.EPIPE: | ||||
raise | ||||
Eric Hopper
|
r2355 | def do_POST(self): | ||
try: | ||||
Brendan Cully
|
r4860 | self.do_write() | ||
Benoit Boissinot
|
r7280 | except StandardError: | ||
Thomas Arendsen Hein
|
r4015 | self._start_response("500 Internal Server Error", []) | ||
self._write("Internal Server Error") | ||||
tb = "".join(traceback.format_exception(*sys.exc_info())) | ||||
Martin Geisler
|
r8663 | self.log_error("Exception happened during processing " | ||
"request '%s':\n%s", self.path, tb) | ||||
Eric Hopper
|
r2355 | |||
def do_GET(self): | ||||
self.do_POST() | ||||
def do_hgweb(self): | ||||
Michele Cella
|
r5835 | path, query = _splitURI(self.path) | ||
Eric Hopper
|
r2355 | |||
env = {} | ||||
env['GATEWAY_INTERFACE'] = 'CGI/1.1' | ||||
env['REQUEST_METHOD'] = self.command | ||||
env['SERVER_NAME'] = self.server.server_name | ||||
env['SERVER_PORT'] = str(self.server.server_port) | ||||
Brendan Cully
|
r3263 | env['REQUEST_URI'] = self.path | ||
Michele Cella
|
r5835 | env['SCRIPT_NAME'] = self.server.prefix | ||
env['PATH_INFO'] = path[len(self.server.prefix):] | ||||
Matt Mackall
|
r4359 | env['REMOTE_HOST'] = self.client_address[0] | ||
env['REMOTE_ADDR'] = self.client_address[0] | ||||
Eric Hopper
|
r2355 | if query: | ||
env['QUERY_STRING'] = query | ||||
if self.headers.typeheader is None: | ||||
env['CONTENT_TYPE'] = self.headers.type | ||||
else: | ||||
env['CONTENT_TYPE'] = self.headers.typeheader | ||||
length = self.headers.getheader('content-length') | ||||
if length: | ||||
env['CONTENT_LENGTH'] = length | ||||
Thomas Arendsen Hein
|
r4633 | for header in [h for h in self.headers.keys() | ||
Eric Hopper
|
r2505 | if h not in ('content-type', 'content-length')]: | ||
hkey = 'HTTP_' + header.replace('-', '_').upper() | ||||
hval = self.headers.getheader(header) | ||||
hval = hval.replace('\n', '').strip() | ||||
if hval: | ||||
env[hkey] = hval | ||||
env['SERVER_PROTOCOL'] = self.request_version | ||||
Eric Hopper
|
r2506 | env['wsgi.version'] = (1, 0) | ||
Brendan Cully
|
r4871 | env['wsgi.url_scheme'] = self.url_scheme | ||
Eric Hopper
|
r2506 | env['wsgi.input'] = self.rfile | ||
env['wsgi.errors'] = _error_logger(self) | ||||
env['wsgi.multithread'] = isinstance(self.server, | ||||
SocketServer.ThreadingMixIn) | ||||
env['wsgi.multiprocess'] = isinstance(self.server, | ||||
SocketServer.ForkingMixIn) | ||||
env['wsgi.run_once'] = 0 | ||||
Eric Hopper
|
r2355 | |||
Eric Hopper
|
r2506 | self.close_connection = True | ||
self.saved_status = None | ||||
self.saved_headers = [] | ||||
self.sent_headers = False | ||||
Eric Hopper
|
r2508 | self.length = None | ||
Dirkjan Ochtman
|
r6784 | for chunk in self.server.application(env, self._start_response): | ||
self._write(chunk) | ||||
Eric Hopper
|
r2506 | |||
def send_headers(self): | ||||
if not self.saved_status: | ||||
Martin Geisler
|
r8663 | raise AssertionError("Sending headers before " | ||
"start_response() called") | ||||
Eric Hopper
|
r2506 | saved_status = self.saved_status.split(None, 1) | ||
saved_status[0] = int(saved_status[0]) | ||||
self.send_response(*saved_status) | ||||
Eric Hopper
|
r2508 | should_close = True | ||
Eric Hopper
|
r2506 | for h in self.saved_headers: | ||
self.send_header(*h) | ||||
Eric Hopper
|
r2508 | if h[0].lower() == 'content-length': | ||
should_close = False | ||||
self.length = int(h[1]) | ||||
Alexis S. L. Carvalho
|
r2582 | # The value of the Connection header is a list of case-insensitive | ||
# tokens separated by commas and optional whitespace. | ||||
Vadim Gelfer
|
r2600 | if 'close' in [token.strip().lower() for token in | ||
Alexis S. L. Carvalho
|
r2582 | self.headers.get('connection', '').split(',')]: | ||
should_close = True | ||||
Eric Hopper
|
r2508 | if should_close: | ||
self.send_header('Connection', 'close') | ||||
self.close_connection = should_close | ||||
Eric Hopper
|
r2506 | self.end_headers() | ||
self.sent_headers = True | ||||
def _start_response(self, http_status, headers, exc_info=None): | ||||
code, msg = http_status.split(None, 1) | ||||
code = int(code) | ||||
self.saved_status = http_status | ||||
Eric Hopper
|
r2508 | bad_headers = ('connection', 'transfer-encoding') | ||
Thomas Arendsen Hein
|
r4633 | self.saved_headers = [h for h in headers | ||
if h[0].lower() not in bad_headers] | ||||
Eric Hopper
|
r2506 | return self._write | ||
def _write(self, data): | ||||
if not self.saved_status: | ||||
raise AssertionError("data written before start_response() called") | ||||
elif not self.sent_headers: | ||||
self.send_headers() | ||||
Eric Hopper
|
r2508 | if self.length is not None: | ||
if len(data) > self.length: | ||||
Martin Geisler
|
r8663 | raise AssertionError("Content-length header sent, but more " | ||
"bytes than specified are being written.") | ||||
Eric Hopper
|
r2508 | self.length = self.length - len(data) | ||
Eric Hopper
|
r2506 | self.wfile.write(data) | ||
self.wfile.flush() | ||||
Eric Hopper
|
r2355 | |||
Brendan Cully
|
r4860 | class _shgwebhandler(_hgwebhandler): | ||
Wesley J. Landaker
|
r4870 | |||
url_scheme = 'https' | ||||
Thomas Arendsen Hein
|
r4957 | |||
Brendan Cully
|
r4860 | def setup(self): | ||
self.connection = self.request | ||||
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) | ||||
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) | ||||
def do_write(self): | ||||
from OpenSSL.SSL import SysCallError | ||||
try: | ||||
super(_shgwebhandler, self).do_write() | ||||
except SysCallError, inst: | ||||
if inst.args[0] != errno.EPIPE: | ||||
raise | ||||
def handle_one_request(self): | ||||
from OpenSSL.SSL import SysCallError, ZeroReturnError | ||||
try: | ||||
super(_shgwebhandler, self).handle_one_request() | ||||
except (SysCallError, ZeroReturnError): | ||||
self.close_connection = True | ||||
pass | ||||
Eric Hopper
|
r2392 | def create_server(ui, repo): | ||
Eric Hopper
|
r2355 | use_threads = True | ||
def openlog(opt, default): | ||||
if opt and opt != '-': | ||||
Mirko Friedenhagen
|
r5690 | return open(opt, 'a') | ||
Eric Hopper
|
r2355 | return default | ||
Eric Hopper
|
r5083 | if repo is None: | ||
Benoit Boissinot
|
r5086 | myui = ui | ||
Eric Hopper
|
r5083 | else: | ||
Benoit Boissinot
|
r5086 | myui = repo.ui | ||
address = myui.config("web", "address", "") | ||||
port = int(myui.config("web", "port", 8000)) | ||||
Michele Cella
|
r5970 | prefix = myui.config("web", "prefix", "") | ||
if prefix: | ||||
prefix = "/" + prefix.strip("/") | ||||
Benoit Boissinot
|
r5086 | use_ipv6 = myui.configbool("web", "ipv6") | ||
webdir_conf = myui.config("web", "webdir_conf") | ||||
Benoit Boissinot
|
r5150 | ssl_cert = myui.config("web", "certificate") | ||
Benoit Boissinot
|
r5086 | accesslog = openlog(myui.config("web", "accesslog", "-"), sys.stdout) | ||
errorlog = openlog(myui.config("web", "errorlog", "-"), sys.stderr) | ||||
Eric Hopper
|
r2355 | |||
if use_threads: | ||||
try: | ||||
from threading import activeCount | ||||
except ImportError: | ||||
use_threads = False | ||||
if use_threads: | ||||
_mixin = SocketServer.ThreadingMixIn | ||||
else: | ||||
if hasattr(os, "fork"): | ||||
_mixin = SocketServer.ForkingMixIn | ||||
else: | ||||
Thomas Arendsen Hein
|
r3673 | class _mixin: | ||
pass | ||||
Eric Hopper
|
r2355 | |||
class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer): | ||||
Thomas Arendsen Hein
|
r4516 | |||
Patrick Mezard
|
r4130 | # SO_REUSEADDR has broken semantics on windows | ||
if os.name == 'nt': | ||||
allow_reuse_address = 0 | ||||
Thomas Arendsen Hein
|
r4516 | |||
Eric Hopper
|
r2355 | def __init__(self, *args, **kargs): | ||
BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs) | ||||
self.accesslog = accesslog | ||||
self.errorlog = errorlog | ||||
Brendan Cully
|
r2650 | self.daemon_threads = True | ||
Alexis S. L. Carvalho
|
r4245 | def make_handler(): | ||
if webdir_conf: | ||||
hgwebobj = hgwebdir(webdir_conf, ui) | ||||
elif repo is not None: | ||||
hgwebobj = hgweb(hg.repository(repo.ui, repo.root)) | ||||
else: | ||||
Matt Mackall
|
r7637 | raise error.RepoError(_("There is no Mercurial repository" | ||
" here (.hg not found)")) | ||||
Alexis S. L. Carvalho
|
r4245 | return hgwebobj | ||
Dirkjan Ochtman
|
r5566 | self.application = make_handler() | ||
Eric Hopper
|
r2355 | |||
Brendan Cully
|
r4860 | if ssl_cert: | ||
try: | ||||
from OpenSSL import SSL | ||||
ctx = SSL.Context(SSL.SSLv23_METHOD) | ||||
except ImportError: | ||||
Martin Geisler
|
r6953 | raise util.Abort(_("SSL support is unavailable")) | ||
Brendan Cully
|
r4860 | ctx.use_privatekey_file(ssl_cert) | ||
ctx.use_certificate_file(ssl_cert) | ||||
sock = socket.socket(self.address_family, self.socket_type) | ||||
self.socket = SSL.Connection(ctx, sock) | ||||
self.server_bind() | ||||
self.server_activate() | ||||
Stephen Deasey
|
r6262 | self.addr, self.port = self.socket.getsockname()[0:2] | ||
self.prefix = prefix | ||||
self.fqaddr = socket.getfqdn(address) | ||||
Eric Hopper
|
r2355 | class IPv6HTTPServer(MercurialHTTPServer): | ||
address_family = getattr(socket, 'AF_INET6', None) | ||||
def __init__(self, *args, **kwargs): | ||||
if self.address_family is None: | ||||
Martin Geisler
|
r7928 | raise error.RepoError(_('IPv6 is not available on this system')) | ||
Eric Hopper
|
r2507 | super(IPv6HTTPServer, self).__init__(*args, **kwargs) | ||
Eric Hopper
|
r2355 | |||
Brendan Cully
|
r4860 | if ssl_cert: | ||
handler = _shgwebhandler | ||||
else: | ||||
handler = _hgwebhandler | ||||
Dirkjan Ochtman
|
r8224 | # ugly hack due to python issue5853 (for threaded use) | ||
import mimetypes; mimetypes.init() | ||||
Matt Mackall
|
r3628 | try: | ||
if use_ipv6: | ||||
Brendan Cully
|
r4860 | return IPv6HTTPServer((address, port), handler) | ||
Matt Mackall
|
r3628 | else: | ||
Brendan Cully
|
r4860 | return MercurialHTTPServer((address, port), handler) | ||
Matt Mackall
|
r3628 | except socket.error, inst: | ||
Stephen Deasey
|
r6262 | raise util.Abort(_("cannot start server at '%s:%d': %s") | ||
% (address, port, inst.args[1])) | ||||