mercurial/hgweb/common.py
changeset 43076 2372284d9457
parent 42926 8d9322b6e687
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    42     Can be overridden by extensions to provide more complex authorization
    42     Can be overridden by extensions to provide more complex authorization
    43     schemes.
    43     schemes.
    44     """
    44     """
    45     return userlist == ['*'] or username in userlist
    45     return userlist == ['*'] or username in userlist
    46 
    46 
       
    47 
    47 def checkauthz(hgweb, req, op):
    48 def checkauthz(hgweb, req, op):
    48     '''Check permission for operation based on request data (including
    49     '''Check permission for operation based on request data (including
    49     authentication info). Return if op allowed, else raise an ErrorResponse
    50     authentication info). Return if op allowed, else raise an ErrorResponse
    50     exception.'''
    51     exception.'''
    51 
    52 
    59     if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
    60     if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
    60         raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
    61         raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
    61 
    62 
    62     if op == 'pull' and not hgweb.allowpull:
    63     if op == 'pull' and not hgweb.allowpull:
    63         raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
    64         raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
    64     elif op == 'pull' or op is None: # op is None for interface requests
    65     elif op == 'pull' or op is None:  # op is None for interface requests
    65         return
    66         return
    66 
    67 
    67     # Allow LFS uploading via PUT requests
    68     # Allow LFS uploading via PUT requests
    68     if op == 'upload':
    69     if op == 'upload':
    69         if req.method != 'PUT':
    70         if req.method != 'PUT':
    84         raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
    85         raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
    85 
    86 
    86     allow = hgweb.configlist('web', 'allow-push')
    87     allow = hgweb.configlist('web', 'allow-push')
    87     if not (allow and ismember(hgweb.repo.ui, user, allow)):
    88     if not (allow and ismember(hgweb.repo.ui, user, allow)):
    88         raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
    89         raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
       
    90 
    89 
    91 
    90 # Hooks for hgweb permission checks; extensions can add hooks here.
    92 # Hooks for hgweb permission checks; extensions can add hooks here.
    91 # Each hook is invoked like this: hook(hgweb, request, operation),
    93 # Each hook is invoked like this: hook(hgweb, request, operation),
    92 # where operation is either read, pull, push or upload. Hooks should either
    94 # where operation is either read, pull, push or upload. Hooks should either
    93 # raise an ErrorResponse exception, or just return.
    95 # raise an ErrorResponse exception, or just return.
   106         if headers is None:
   108         if headers is None:
   107             headers = []
   109             headers = []
   108         self.headers = headers
   110         self.headers = headers
   109         self.message = message
   111         self.message = message
   110 
   112 
       
   113 
   111 class continuereader(object):
   114 class continuereader(object):
   112     """File object wrapper to handle HTTP 100-continue.
   115     """File object wrapper to handle HTTP 100-continue.
   113 
   116 
   114     This is used by servers so they automatically handle Expect: 100-continue
   117     This is used by servers so they automatically handle Expect: 100-continue
   115     request headers. On first read of the request body, the 100 Continue
   118     request headers. On first read of the request body, the 100 Continue
   116     response is sent. This should trigger the client into actually sending
   119     response is sent. This should trigger the client into actually sending
   117     the request body.
   120     the request body.
   118     """
   121     """
       
   122 
   119     def __init__(self, f, write):
   123     def __init__(self, f, write):
   120         self.f = f
   124         self.f = f
   121         self._write = write
   125         self._write = write
   122         self.continued = False
   126         self.continued = False
   123 
   127 
   130     def __getattr__(self, attr):
   134     def __getattr__(self, attr):
   131         if attr in ('close', 'readline', 'readlines', '__iter__'):
   135         if attr in ('close', 'readline', 'readlines', '__iter__'):
   132             return getattr(self.f, attr)
   136             return getattr(self.f, attr)
   133         raise AttributeError
   137         raise AttributeError
   134 
   138 
       
   139 
   135 def _statusmessage(code):
   140 def _statusmessage(code):
   136     responses = httpserver.basehttprequesthandler.responses
   141     responses = httpserver.basehttprequesthandler.responses
   137     return pycompat.bytesurl(
   142     return pycompat.bytesurl(
   138         responses.get(code, (r'Error', r'Unknown error'))[0])
   143         responses.get(code, (r'Error', r'Unknown error'))[0]
       
   144     )
       
   145 
   139 
   146 
   140 def statusmessage(code, message=None):
   147 def statusmessage(code, message=None):
   141     return '%d %s' % (code, message or _statusmessage(code))
   148     return '%d %s' % (code, message or _statusmessage(code))
       
   149 
   142 
   150 
   143 def get_stat(spath, fn):
   151 def get_stat(spath, fn):
   144     """stat fn if it exists, spath otherwise"""
   152     """stat fn if it exists, spath otherwise"""
   145     cl_path = os.path.join(spath, fn)
   153     cl_path = os.path.join(spath, fn)
   146     if os.path.exists(cl_path):
   154     if os.path.exists(cl_path):
   147         return os.stat(cl_path)
   155         return os.stat(cl_path)
   148     else:
   156     else:
   149         return os.stat(spath)
   157         return os.stat(spath)
   150 
   158 
       
   159 
   151 def get_mtime(spath):
   160 def get_mtime(spath):
   152     return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
   161     return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
       
   162 
   153 
   163 
   154 def ispathsafe(path):
   164 def ispathsafe(path):
   155     """Determine if a path is safe to use for filesystem access."""
   165     """Determine if a path is safe to use for filesystem access."""
   156     parts = path.split('/')
   166     parts = path.split('/')
   157     for part in parts:
   167     for part in parts:
   158         if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
   168         if (
   159             pycompat.ossep in part or
   169             part in ('', pycompat.oscurdir, pycompat.ospardir)
   160             pycompat.osaltsep is not None and pycompat.osaltsep in part):
   170             or pycompat.ossep in part
       
   171             or pycompat.osaltsep is not None
       
   172             and pycompat.osaltsep in part
       
   173         ):
   161             return False
   174             return False
   162 
   175 
   163     return True
   176     return True
       
   177 
   164 
   178 
   165 def staticfile(directory, fname, res):
   179 def staticfile(directory, fname, res):
   166     """return a file inside directory with guessed Content-Type header
   180     """return a file inside directory with guessed Content-Type header
   167 
   181 
   168     fname always uses '/' as directory separator and isn't allowed to
   182     fname always uses '/' as directory separator and isn't allowed to
   182         if os.path.exists(path):
   196         if os.path.exists(path):
   183             break
   197             break
   184     try:
   198     try:
   185         os.stat(path)
   199         os.stat(path)
   186         ct = pycompat.sysbytes(
   200         ct = pycompat.sysbytes(
   187             mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain")
   201             mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain"
       
   202         )
   188         with open(path, 'rb') as fh:
   203         with open(path, 'rb') as fh:
   189             data = fh.read()
   204             data = fh.read()
   190 
   205 
   191         res.headers['Content-Type'] = ct
   206         res.headers['Content-Type'] = ct
   192         res.setbodybytes(data)
   207         res.setbodybytes(data)
   195         raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
   210         raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
   196     except OSError as err:
   211     except OSError as err:
   197         if err.errno == errno.ENOENT:
   212         if err.errno == errno.ENOENT:
   198             raise ErrorResponse(HTTP_NOT_FOUND)
   213             raise ErrorResponse(HTTP_NOT_FOUND)
   199         else:
   214         else:
   200             raise ErrorResponse(HTTP_SERVER_ERROR,
   215             raise ErrorResponse(
   201                                 encoding.strtolocal(err.strerror))
   216                 HTTP_SERVER_ERROR, encoding.strtolocal(err.strerror)
       
   217             )
       
   218 
   202 
   219 
   203 def paritygen(stripecount, offset=0):
   220 def paritygen(stripecount, offset=0):
   204     """count parity of horizontal stripes for easier reading"""
   221     """count parity of horizontal stripes for easier reading"""
   205     if stripecount and offset:
   222     if stripecount and offset:
   206         # account for offset, e.g. due to building the list in reverse
   223         # account for offset, e.g. due to building the list in reverse
   214         count += 1
   231         count += 1
   215         if stripecount and count >= stripecount:
   232         if stripecount and count >= stripecount:
   216             parity = 1 - parity
   233             parity = 1 - parity
   217             count = 0
   234             count = 0
   218 
   235 
       
   236 
   219 def get_contact(config):
   237 def get_contact(config):
   220     """Return repo contact information or empty string.
   238     """Return repo contact information or empty string.
   221 
   239 
   222     web.contact is the primary source, but if that is not set, try
   240     web.contact is the primary source, but if that is not set, try
   223     ui.username or $EMAIL as a fallback to display something useful.
   241     ui.username or $EMAIL as a fallback to display something useful.
   224     """
   242     """
   225     return (config("web", "contact") or
   243     return (
   226             config("ui", "username") or
   244         config("web", "contact")
   227             encoding.environ.get("EMAIL") or "")
   245         or config("ui", "username")
       
   246         or encoding.environ.get("EMAIL")
       
   247         or ""
       
   248     )
       
   249 
   228 
   250 
   229 def cspvalues(ui):
   251 def cspvalues(ui):
   230     """Obtain the Content-Security-Policy header and nonce value.
   252     """Obtain the Content-Security-Policy header and nonce value.
   231 
   253 
   232     Returns a 2-tuple of the CSP header value and the nonce value.
   254     Returns a 2-tuple of the CSP header value and the nonce value.