PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/mercurial/hgweb/common.py

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