mercurial/hgweb/common.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43085 eef9a2d67051
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    40 
    40 
    41     If userlist has a single '*' member, all users are considered members.
    41     If userlist has a single '*' member, all users are considered members.
    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 == [b'*'] or username in userlist
    46 
    46 
    47 
    47 
    48 def checkauthz(hgweb, req, op):
    48 def checkauthz(hgweb, req, op):
    49     '''Check permission for operation based on request data (including
    49     '''Check permission for operation based on request data (including
    50     authentication info). Return if op allowed, else raise an ErrorResponse
    50     authentication info). Return if op allowed, else raise an ErrorResponse
    51     exception.'''
    51     exception.'''
    52 
    52 
    53     user = req.remoteuser
    53     user = req.remoteuser
    54 
    54 
    55     deny_read = hgweb.configlist('web', 'deny_read')
    55     deny_read = hgweb.configlist(b'web', b'deny_read')
    56     if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
    56     if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
    57         raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
    57         raise ErrorResponse(HTTP_UNAUTHORIZED, b'read not authorized')
    58 
    58 
    59     allow_read = hgweb.configlist('web', 'allow_read')
    59     allow_read = hgweb.configlist(b'web', b'allow_read')
    60     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)):
    61         raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
    61         raise ErrorResponse(HTTP_UNAUTHORIZED, b'read not authorized')
    62 
    62 
    63     if op == 'pull' and not hgweb.allowpull:
    63     if op == b'pull' and not hgweb.allowpull:
    64         raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
    64         raise ErrorResponse(HTTP_UNAUTHORIZED, b'pull not authorized')
    65     elif op == 'pull' or op is None:  # op is None for interface requests
    65     elif op == b'pull' or op is None:  # op is None for interface requests
    66         return
    66         return
    67 
    67 
    68     # Allow LFS uploading via PUT requests
    68     # Allow LFS uploading via PUT requests
    69     if op == 'upload':
    69     if op == b'upload':
    70         if req.method != 'PUT':
    70         if req.method != b'PUT':
    71             msg = 'upload requires PUT request'
    71             msg = b'upload requires PUT request'
    72             raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
    72             raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
    73     # enforce that you can only push using POST requests
    73     # enforce that you can only push using POST requests
    74     elif req.method != 'POST':
    74     elif req.method != b'POST':
    75         msg = 'push requires POST request'
    75         msg = b'push requires POST request'
    76         raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
    76         raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
    77 
    77 
    78     # require ssl by default for pushing, auth info cannot be sniffed
    78     # require ssl by default for pushing, auth info cannot be sniffed
    79     # and replayed
    79     # and replayed
    80     if hgweb.configbool('web', 'push_ssl') and req.urlscheme != 'https':
    80     if hgweb.configbool(b'web', b'push_ssl') and req.urlscheme != b'https':
    81         raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
    81         raise ErrorResponse(HTTP_FORBIDDEN, b'ssl required')
    82 
    82 
    83     deny = hgweb.configlist('web', 'deny_push')
    83     deny = hgweb.configlist(b'web', b'deny_push')
    84     if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
    84     if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
    85         raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
    85         raise ErrorResponse(HTTP_UNAUTHORIZED, b'push not authorized')
    86 
    86 
    87     allow = hgweb.configlist('web', 'allow-push')
    87     allow = hgweb.configlist(b'web', b'allow-push')
    88     if not (allow and ismember(hgweb.repo.ui, user, allow)):
    88     if not (allow and ismember(hgweb.repo.ui, user, allow)):
    89         raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
    89         raise ErrorResponse(HTTP_UNAUTHORIZED, b'push not authorized')
    90 
    90 
    91 
    91 
    92 # Hooks for hgweb permission checks; extensions can add hooks here.
    92 # Hooks for hgweb permission checks; extensions can add hooks here.
    93 # Each hook is invoked like this: hook(hgweb, request, operation),
    93 # Each hook is invoked like this: hook(hgweb, request, operation),
    94 # 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
   126         self.continued = False
   126         self.continued = False
   127 
   127 
   128     def read(self, amt=-1):
   128     def read(self, amt=-1):
   129         if not self.continued:
   129         if not self.continued:
   130             self.continued = True
   130             self.continued = True
   131             self._write('HTTP/1.1 100 Continue\r\n\r\n')
   131             self._write(b'HTTP/1.1 100 Continue\r\n\r\n')
   132         return self.f.read(amt)
   132         return self.f.read(amt)
   133 
   133 
   134     def __getattr__(self, attr):
   134     def __getattr__(self, attr):
   135         if attr in ('close', 'readline', 'readlines', '__iter__'):
   135         if attr in (b'close', b'readline', b'readlines', b'__iter__'):
   136             return getattr(self.f, attr)
   136             return getattr(self.f, attr)
   137         raise AttributeError
   137         raise AttributeError
   138 
   138 
   139 
   139 
   140 def _statusmessage(code):
   140 def _statusmessage(code):
   143         responses.get(code, (r'Error', r'Unknown error'))[0]
   143         responses.get(code, (r'Error', r'Unknown error'))[0]
   144     )
   144     )
   145 
   145 
   146 
   146 
   147 def statusmessage(code, message=None):
   147 def statusmessage(code, message=None):
   148     return '%d %s' % (code, message or _statusmessage(code))
   148     return b'%d %s' % (code, message or _statusmessage(code))
   149 
   149 
   150 
   150 
   151 def get_stat(spath, fn):
   151 def get_stat(spath, fn):
   152     """stat fn if it exists, spath otherwise"""
   152     """stat fn if it exists, spath otherwise"""
   153     cl_path = os.path.join(spath, fn)
   153     cl_path = os.path.join(spath, fn)
   156     else:
   156     else:
   157         return os.stat(spath)
   157         return os.stat(spath)
   158 
   158 
   159 
   159 
   160 def get_mtime(spath):
   160 def get_mtime(spath):
   161     return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
   161     return get_stat(spath, b"00changelog.i")[stat.ST_MTIME]
   162 
   162 
   163 
   163 
   164 def ispathsafe(path):
   164 def ispathsafe(path):
   165     """Determine if a path is safe to use for filesystem access."""
   165     """Determine if a path is safe to use for filesystem access."""
   166     parts = path.split('/')
   166     parts = path.split(b'/')
   167     for part in parts:
   167     for part in parts:
   168         if (
   168         if (
   169             part in ('', pycompat.oscurdir, pycompat.ospardir)
   169             part in (b'', pycompat.oscurdir, pycompat.ospardir)
   170             or pycompat.ossep in part
   170             or pycompat.ossep in part
   171             or pycompat.osaltsep is not None
   171             or pycompat.osaltsep is not None
   172             and pycompat.osaltsep in part
   172             and pycompat.osaltsep in part
   173         ):
   173         ):
   174             return False
   174             return False
   186 
   186 
   187     """
   187     """
   188     if not ispathsafe(fname):
   188     if not ispathsafe(fname):
   189         return
   189         return
   190 
   190 
   191     fpath = os.path.join(*fname.split('/'))
   191     fpath = os.path.join(*fname.split(b'/'))
   192     if isinstance(directory, str):
   192     if isinstance(directory, str):
   193         directory = [directory]
   193         directory = [directory]
   194     for d in directory:
   194     for d in directory:
   195         path = os.path.join(d, fpath)
   195         path = os.path.join(d, fpath)
   196         if os.path.exists(path):
   196         if os.path.exists(path):
   198     try:
   198     try:
   199         os.stat(path)
   199         os.stat(path)
   200         ct = pycompat.sysbytes(
   200         ct = pycompat.sysbytes(
   201             mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain"
   201             mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain"
   202         )
   202         )
   203         with open(path, 'rb') as fh:
   203         with open(path, b'rb') as fh:
   204             data = fh.read()
   204             data = fh.read()
   205 
   205 
   206         res.headers['Content-Type'] = ct
   206         res.headers[b'Content-Type'] = ct
   207         res.setbodybytes(data)
   207         res.setbodybytes(data)
   208         return res
   208         return res
   209     except TypeError:
   209     except TypeError:
   210         raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
   210         raise ErrorResponse(HTTP_SERVER_ERROR, b'illegal filename')
   211     except OSError as err:
   211     except OSError as err:
   212         if err.errno == errno.ENOENT:
   212         if err.errno == errno.ENOENT:
   213             raise ErrorResponse(HTTP_NOT_FOUND)
   213             raise ErrorResponse(HTTP_NOT_FOUND)
   214         else:
   214         else:
   215             raise ErrorResponse(
   215             raise ErrorResponse(
   239 
   239 
   240     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
   241     ui.username or $EMAIL as a fallback to display something useful.
   241     ui.username or $EMAIL as a fallback to display something useful.
   242     """
   242     """
   243     return (
   243     return (
   244         config("web", "contact")
   244         config(b"web", b"contact")
   245         or config("ui", "username")
   245         or config(b"ui", b"username")
   246         or encoding.environ.get("EMAIL")
   246         or encoding.environ.get(b"EMAIL")
   247         or ""
   247         or b""
   248     )
   248     )
   249 
   249 
   250 
   250 
   251 def cspvalues(ui):
   251 def cspvalues(ui):
   252     """Obtain the Content-Security-Policy header and nonce value.
   252     """Obtain the Content-Security-Policy header and nonce value.
   273     # We can move it back once we no longer need Python <= 2.7.12 support.
   273     # We can move it back once we no longer need Python <= 2.7.12 support.
   274     import uuid
   274     import uuid
   275 
   275 
   276     # Don't allow untrusted CSP setting since it be disable protections
   276     # Don't allow untrusted CSP setting since it be disable protections
   277     # from a trusted/global source.
   277     # from a trusted/global source.
   278     csp = ui.config('web', 'csp', untrusted=False)
   278     csp = ui.config(b'web', b'csp', untrusted=False)
   279     nonce = None
   279     nonce = None
   280 
   280 
   281     if csp and '%nonce%' in csp:
   281     if csp and b'%nonce%' in csp:
   282         nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
   282         nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip(b'=')
   283         csp = csp.replace('%nonce%', nonce)
   283         csp = csp.replace(b'%nonce%', nonce)
   284 
   284 
   285     return csp, nonce
   285     return csp, nonce