PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/hgweb/server.py

https://bitbucket.org/mirror/mercurial/
Python | 347 lines | 328 code | 4 blank | 15 comment | 2 complexity | 9648ca15bd3f836b318aa3f28cae43dc MD5 | raw file
Possible License(s): GPL-2.0
  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 or any later version.
  8. import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
  9. from mercurial import util, error
  10. from mercurial.hgweb import common
  11. from mercurial.i18n import _
  12. def _splitURI(uri):
  13. """Return path and query that has been split from uri
  14. Just like CGI environment, the path is unquoted, the query is
  15. not.
  16. """
  17. if '?' in uri:
  18. path, query = uri.split('?', 1)
  19. else:
  20. path, query = uri, ''
  21. return urllib.unquote(path), query
  22. class _error_logger(object):
  23. def __init__(self, handler):
  24. self.handler = handler
  25. def flush(self):
  26. pass
  27. def write(self, str):
  28. self.writelines(str.split('\n'))
  29. def writelines(self, seq):
  30. for msg in seq:
  31. self.handler.log_error("HG error: %s", msg)
  32. class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
  33. url_scheme = 'http'
  34. @staticmethod
  35. def preparehttpserver(httpserver, ssl_cert):
  36. """Prepare .socket of new HTTPServer instance"""
  37. pass
  38. def __init__(self, *args, **kargs):
  39. self.protocol_version = 'HTTP/1.1'
  40. BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
  41. def _log_any(self, fp, format, *args):
  42. fp.write("%s - - [%s] %s\n" % (self.client_address[0],
  43. self.log_date_time_string(),
  44. format % args))
  45. fp.flush()
  46. def log_error(self, format, *args):
  47. self._log_any(self.server.errorlog, format, *args)
  48. def log_message(self, format, *args):
  49. self._log_any(self.server.accesslog, format, *args)
  50. def log_request(self, code='-', size='-'):
  51. xheaders = []
  52. if util.safehasattr(self, 'headers'):
  53. xheaders = [h for h in self.headers.items()
  54. if h[0].startswith('x-')]
  55. self.log_message('"%s" %s %s%s',
  56. self.requestline, str(code), str(size),
  57. ''.join([' %s:%s' % h for h in sorted(xheaders)]))
  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. def do_POST(self):
  65. try:
  66. self.do_write()
  67. except Exception:
  68. self._start_response("500 Internal Server Error", [])
  69. self._write("Internal Server Error")
  70. tb = "".join(traceback.format_exception(*sys.exc_info()))
  71. self.log_error("Exception happened during processing "
  72. "request '%s':\n%s", self.path, tb)
  73. def do_GET(self):
  74. self.do_POST()
  75. def do_hgweb(self):
  76. path, query = _splitURI(self.path)
  77. env = {}
  78. env['GATEWAY_INTERFACE'] = 'CGI/1.1'
  79. env['REQUEST_METHOD'] = self.command
  80. env['SERVER_NAME'] = self.server.server_name
  81. env['SERVER_PORT'] = str(self.server.server_port)
  82. env['REQUEST_URI'] = self.path
  83. env['SCRIPT_NAME'] = self.server.prefix
  84. env['PATH_INFO'] = path[len(self.server.prefix):]
  85. env['REMOTE_HOST'] = self.client_address[0]
  86. env['REMOTE_ADDR'] = self.client_address[0]
  87. if query:
  88. env['QUERY_STRING'] = query
  89. if self.headers.typeheader is None:
  90. env['CONTENT_TYPE'] = self.headers.type
  91. else:
  92. env['CONTENT_TYPE'] = self.headers.typeheader
  93. length = self.headers.getheader('content-length')
  94. if length:
  95. env['CONTENT_LENGTH'] = length
  96. for header in [h for h in self.headers.keys()
  97. if h not in ('content-type', 'content-length')]:
  98. hkey = 'HTTP_' + header.replace('-', '_').upper()
  99. hval = self.headers.getheader(header)
  100. hval = hval.replace('\n', '').strip()
  101. if hval:
  102. env[hkey] = hval
  103. env['SERVER_PROTOCOL'] = self.request_version
  104. env['wsgi.version'] = (1, 0)
  105. env['wsgi.url_scheme'] = self.url_scheme
  106. if env.get('HTTP_EXPECT', '').lower() == '100-continue':
  107. self.rfile = common.continuereader(self.rfile, self.wfile.write)
  108. env['wsgi.input'] = self.rfile
  109. env['wsgi.errors'] = _error_logger(self)
  110. env['wsgi.multithread'] = isinstance(self.server,
  111. SocketServer.ThreadingMixIn)
  112. env['wsgi.multiprocess'] = isinstance(self.server,
  113. SocketServer.ForkingMixIn)
  114. env['wsgi.run_once'] = 0
  115. self.saved_status = None
  116. self.saved_headers = []
  117. self.sent_headers = False
  118. self.length = None
  119. self._chunked = None
  120. for chunk in self.server.application(env, self._start_response):
  121. self._write(chunk)
  122. if not self.sent_headers:
  123. self.send_headers()
  124. self._done()
  125. def send_headers(self):
  126. if not self.saved_status:
  127. raise AssertionError("Sending headers before "
  128. "start_response() called")
  129. saved_status = self.saved_status.split(None, 1)
  130. saved_status[0] = int(saved_status[0])
  131. self.send_response(*saved_status)
  132. self.length = None
  133. self._chunked = False
  134. for h in self.saved_headers:
  135. self.send_header(*h)
  136. if h[0].lower() == 'content-length':
  137. self.length = int(h[1])
  138. if (self.length is None and
  139. saved_status[0] != common.HTTP_NOT_MODIFIED):
  140. self._chunked = (not self.close_connection and
  141. self.request_version == "HTTP/1.1")
  142. if self._chunked:
  143. self.send_header('Transfer-Encoding', 'chunked')
  144. else:
  145. self.send_header('Connection', 'close')
  146. self.end_headers()
  147. self.sent_headers = True
  148. def _start_response(self, http_status, headers, exc_info=None):
  149. code, msg = http_status.split(None, 1)
  150. code = int(code)
  151. self.saved_status = http_status
  152. bad_headers = ('connection', 'transfer-encoding')
  153. self.saved_headers = [h for h in headers
  154. if h[0].lower() not in bad_headers]
  155. return self._write
  156. def _write(self, data):
  157. if not self.saved_status:
  158. raise AssertionError("data written before start_response() called")
  159. elif not self.sent_headers:
  160. self.send_headers()
  161. if self.length is not None:
  162. if len(data) > self.length:
  163. raise AssertionError("Content-length header sent, but more "
  164. "bytes than specified are being written.")
  165. self.length = self.length - len(data)
  166. elif self._chunked and data:
  167. data = '%x\r\n%s\r\n' % (len(data), data)
  168. self.wfile.write(data)
  169. self.wfile.flush()
  170. def _done(self):
  171. if self._chunked:
  172. self.wfile.write('0\r\n\r\n')
  173. self.wfile.flush()
  174. class _httprequesthandleropenssl(_httprequesthandler):
  175. """HTTPS handler based on pyOpenSSL"""
  176. url_scheme = 'https'
  177. @staticmethod
  178. def preparehttpserver(httpserver, ssl_cert):
  179. try:
  180. import OpenSSL
  181. OpenSSL.SSL.Context
  182. except ImportError:
  183. raise util.Abort(_("SSL support is unavailable"))
  184. ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
  185. ctx.use_privatekey_file(ssl_cert)
  186. ctx.use_certificate_file(ssl_cert)
  187. sock = socket.socket(httpserver.address_family, httpserver.socket_type)
  188. httpserver.socket = OpenSSL.SSL.Connection(ctx, sock)
  189. httpserver.server_bind()
  190. httpserver.server_activate()
  191. def setup(self):
  192. self.connection = self.request
  193. self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
  194. self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
  195. def do_write(self):
  196. import OpenSSL
  197. try:
  198. _httprequesthandler.do_write(self)
  199. except OpenSSL.SSL.SysCallError, inst:
  200. if inst.args[0] != errno.EPIPE:
  201. raise
  202. def handle_one_request(self):
  203. import OpenSSL
  204. try:
  205. _httprequesthandler.handle_one_request(self)
  206. except (OpenSSL.SSL.SysCallError, OpenSSL.SSL.ZeroReturnError):
  207. self.close_connection = True
  208. pass
  209. class _httprequesthandlerssl(_httprequesthandler):
  210. """HTTPS handler based on Pythons ssl module (introduced in 2.6)"""
  211. url_scheme = 'https'
  212. @staticmethod
  213. def preparehttpserver(httpserver, ssl_cert):
  214. try:
  215. import ssl
  216. ssl.wrap_socket
  217. except ImportError:
  218. raise util.Abort(_("SSL support is unavailable"))
  219. httpserver.socket = ssl.wrap_socket(httpserver.socket, server_side=True,
  220. certfile=ssl_cert, ssl_version=ssl.PROTOCOL_SSLv23)
  221. def setup(self):
  222. self.connection = self.request
  223. self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
  224. self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
  225. try:
  226. from threading import activeCount
  227. activeCount() # silence pyflakes
  228. _mixin = SocketServer.ThreadingMixIn
  229. except ImportError:
  230. if util.safehasattr(os, "fork"):
  231. _mixin = SocketServer.ForkingMixIn
  232. else:
  233. class _mixin(object):
  234. pass
  235. def openlog(opt, default):
  236. if opt and opt != '-':
  237. return open(opt, 'a')
  238. return default
  239. class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
  240. # SO_REUSEADDR has broken semantics on windows
  241. if os.name == 'nt':
  242. allow_reuse_address = 0
  243. def __init__(self, ui, app, addr, handler, **kwargs):
  244. BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
  245. self.daemon_threads = True
  246. self.application = app
  247. handler.preparehttpserver(self, ui.config('web', 'certificate'))
  248. prefix = ui.config('web', 'prefix', '')
  249. if prefix:
  250. prefix = '/' + prefix.strip('/')
  251. self.prefix = prefix
  252. alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
  253. elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
  254. self.accesslog = alog
  255. self.errorlog = elog
  256. self.addr, self.port = self.socket.getsockname()[0:2]
  257. self.fqaddr = socket.getfqdn(addr[0])
  258. class IPv6HTTPServer(MercurialHTTPServer):
  259. address_family = getattr(socket, 'AF_INET6', None)
  260. def __init__(self, *args, **kwargs):
  261. if self.address_family is None:
  262. raise error.RepoError(_('IPv6 is not available on this system'))
  263. super(IPv6HTTPServer, self).__init__(*args, **kwargs)
  264. def create_server(ui, app):
  265. if ui.config('web', 'certificate'):
  266. if sys.version_info >= (2, 6):
  267. handler = _httprequesthandlerssl
  268. else:
  269. handler = _httprequesthandleropenssl
  270. else:
  271. handler = _httprequesthandler
  272. if ui.configbool('web', 'ipv6'):
  273. cls = IPv6HTTPServer
  274. else:
  275. cls = MercurialHTTPServer
  276. # ugly hack due to python issue5853 (for threaded use)
  277. try:
  278. import mimetypes
  279. mimetypes.init()
  280. except UnicodeDecodeError:
  281. # Python 2.x's mimetypes module attempts to decode strings
  282. # from Windows' ANSI APIs as ascii (fail), then re-encode them
  283. # as ascii (clown fail), because the default Python Unicode
  284. # codec is hardcoded as ascii.
  285. sys.argv # unwrap demand-loader so that reload() works
  286. reload(sys) # resurrect sys.setdefaultencoding()
  287. oldenc = sys.getdefaultencoding()
  288. sys.setdefaultencoding("latin1") # or any full 8-bit encoding
  289. mimetypes.init()
  290. sys.setdefaultencoding(oldenc)
  291. address = ui.config('web', 'address', '')
  292. port = util.getport(ui.config('web', 'port', 8000))
  293. try:
  294. return cls(ui, app, (address, port), handler)
  295. except socket.error, inst:
  296. raise util.Abort(_("cannot start server at '%s:%d': %s")
  297. % (address, port, inst.args[1]))