def create_item(self, path): """Creates a new Location object for the site. The location path should be canonical and should not contain parts that are not used for access control (query, fragment, parameters). Location should not contain non-ascii characters. Raises: ValidationError if the path is invalid or if a site already has a location with such path. LimitExceeded if the site defines a maximum number of locations and adding a new one would exceed this number. """ locations_limit = self.site.locations_limit if (locations_limit is not None and self.count() >= locations_limit): raise LimitExceeded('Locations limit exceeded') if not url_utils.is_canonical(path): raise ValidationError( 'Path should be absolute and normalized (starting with / '\ 'without /../ or /./ or //).') if len(path) > self.PATH_LEN_LIMIT: raise ValidationError('Path too long') if url_utils.contains_fragment(path): raise ValidationError( "Path should not contain fragment ('#' part).") if url_utils.contains_query(path): raise ValidationError( "Path should not contain query ('?' part).") if url_utils.contains_params(path): raise ValidationError( "Path should not contain parameters (';' part).") try: path.encode('ascii') except UnicodeError: raise ValidationError( 'Path should contain only ascii characters.') if self.get_unique(lambda item: item.path == path) is not None: raise ValidationError('Location already exists.') return self._do_create_item(path=path)
def create_item(self, path): """Creates a new Location object for the site. The location path should be canonical and should not contain parts that are not used for access control (query, fragment, parameters). Location should not contain non-ascii characters. Raises: ValidationError if the path is invalid or if a site already has a location with such path. LimitExceeded if the site defines a maximum number of locations and adding a new one would exceed this number. """ locations_limit = self.site.locations_limit if (locations_limit is not None and self.count() >= locations_limit): raise LimitExceeded('Locations limit exceeded') if not url_utils.is_canonical(path): raise ValidationError( 'Path should be absolute and normalized (starting with / '\ 'without /../ or /./ or //).') if len(path) > self.PATH_LEN_LIMIT: raise ValidationError('Path too long') if url_utils.contains_fragment(path): raise ValidationError( "Path should not contain fragment ('#' part).") if url_utils.contains_query(path): raise ValidationError("Path should not contain query ('?' part).") if url_utils.contains_params(path): raise ValidationError( "Path should not contain parameters (';' part).") try: path.encode('ascii') except UnicodeError: raise ValidationError('Path should contain only ascii characters.') if self.get_unique(lambda item: item.path == path) is not None: raise ValidationError('Location already exists.') return self._do_create_item(path=path)
def get(self, request): """Invoked by the HTTP server with a single path argument. The HTTP server should pass the path argument verbatim, without any transformations or decoding. Access control mechanism should work on user visible paths, not paths after internal rewrites performed by the server. At the moment, the path is allowed to contain a query part, which is ignored (this is because nginx does not expose encoded path without the query part). The method follows be conservative in what you accept principle. The path should be absolute and normalized, without fragment id, otherwise access is denied. Browsers in normal operations perform path normalization and do not send fragment id. Multiple consecutive '/' separators are permitted, because these are not normalized by browsers, and are used by legitimate applications. Paths with '/./' and '/../', should not be normally sent by browsers and can be a sign of something suspicious happening. It is extremely important that wwwhisper does not perform any path transformations that are not be compatible with transformations done by the HTTP server. """ encoded_path = self._extract_encoded_path_argument(request) if encoded_path is None: return http.HttpResponseBadRequest( "Auth request should have 'path' argument.") # Do not allow requests that contain the 'User' header. The # header is passed to backends and must be guaranteed to be # set by wwwhisper. # This check should already be performed by HTTP server. if 'HTTP_USER' in request.META: return http.HttpResponseBadRequest( "Client can not set the 'User' header") debug_msg = "Auth request to '%s'" % (encoded_path) path_validation_error = None if url_utils.contains_fragment(encoded_path): path_validation_error = "Path should not include fragment ('#')" else: stripped_path = url_utils.strip_query(encoded_path) decoded_path = url_utils.decode(stripped_path) decoded_path = url_utils.collapse_slashes(decoded_path) if not url_utils.is_canonical(decoded_path): path_validation_error = 'Path should be absolute and ' \ 'normalized (starting with / without /../ or /./ or //).' if path_validation_error is not None: logger.debug('%s: incorrect path.' % (debug_msg)) return http.HttpResponseBadRequest(path_validation_error) user = _get_user(request) location = request.site.locations.find_location(decoded_path) if user is not None: debug_msg += " by '%s'" % (user.email) respone = None if location is not None and location.can_access(user): logger.debug('%s: access granted.' % (debug_msg)) response = http.HttpResponseOK('Access granted.') else: logger.debug('%s: access denied.' % (debug_msg)) response = http.HttpResponseNotAuthorized( _html_or_none(request, 'not_authorized.html', {'email': user.email})) response['User'] = user.email return response if (location is not None and location.open_access_granted() and not location.open_access_requires_login()): logger.debug('%s: authentication not required, access granted.' % (debug_msg)) return http.HttpResponseOK('Access granted.') logger.debug('%s: user not authenticated.' % (debug_msg)) return http.HttpResponseNotAuthenticated( _html_or_none(request, 'login.html', request.site.skin()))
def test_contains_fragment(self): self.assertTrue(contains_fragment('/foo#123')) self.assertTrue(contains_fragment('/foo#')) # Encoded '#' should not be treated as fragment separator. self.assertFalse(contains_fragment('/foo%23'))
def get(self, request): """Invoked by the HTTP server with a single path argument. The HTTP server should pass the path argument verbatim, without any transformations or decoding. Access control mechanism should work on user visible paths, not paths after internal rewrites performed by the server. At the moment, the path is allowed to contain a query part, which is ignored (this is because nginx does not expose encoded path without the query part). The method follows be conservative in what you accept principle. The path should be absolute and normalized, without fragment id, otherwise access is denied. Browsers in normal operations perform path normalization and do not send fragment id. Multiple consecutive '/' separators are permitted, because these are not normalized by browsers, and are used by legitimate applications. Paths with '/./' and '/../', should not be normally sent by browsers and can be a sign of something suspicious happening. It is extremely important that wwwhisper does not perform any path transformations that are not be compatible with transformations done by the HTTP server. """ encoded_path = self._extract_encoded_path_argument(request) if encoded_path is None: return http.HttpResponseBadRequest( "Auth request should have 'path' argument.") # Do not allow requests that contain the 'User' header. The # header is passed to backends and must be guaranteed to be # set by wwwhisper. # This check should already be performed by HTTP server. if 'HTTP_USER' in request.META: return http.HttpResponseBadRequest( "Client can not set the 'User' header") debug_msg = "Auth request to '%s'" % (encoded_path) path_validation_error = None if url_utils.contains_fragment(encoded_path): path_validation_error = "Path should not include fragment ('#')" else: stripped_path = url_utils.strip_query(encoded_path) decoded_path = url_utils.decode(stripped_path) decoded_path = url_utils.collapse_slashes(decoded_path) if not url_utils.is_canonical(decoded_path): path_validation_error = 'Path should be absolute and ' \ 'normalized (starting with / without /../ or /./ or //).' if path_validation_error is not None: logger.debug('%s: incorrect path.' % (debug_msg)) return http.HttpResponseBadRequest(path_validation_error) user = _get_user(request) location = request.site.locations.find_location(decoded_path) if user is not None: debug_msg += " by '%s'" % (user.email) respone = None if location is not None and location.can_access(user): logger.debug('%s: access granted.' % (debug_msg)) response = http.HttpResponseOK('Access granted.') else: logger.debug('%s: access denied.' % (debug_msg)) response = http.HttpResponseNotAuthorized( _html_or_none(request, 'not_authorized.html', {'email' : user.email})) response['User'] = user.email return response if (location is not None and location.open_access_granted() and not location.open_access_requires_login()): logger.debug('%s: authentication not required, access granted.' % (debug_msg)) return http.HttpResponseOK('Access granted.') logger.debug('%s: user not authenticated.' % (debug_msg)) return http.HttpResponseNotAuthenticated( _html_or_none(request, 'login.html', request.site.skin()))