Refactor for all middlewares (now are all on same place..)

This commit is contained in:
Adolfo Gómez García 2021-06-01 13:17:53 +02:00
parent 394ceb9e66
commit 21f6df36b0
7 changed files with 331 additions and 224 deletions

View File

@ -175,9 +175,9 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'uds.core.util.request.GlobalRequestMiddleware',
'uds.core.util.middleware.XUACompatibleMiddleware',
'uds.core.util.middleware.RedirectMiddleware',
'uds.core.util.middleware.request.GlobalRequestMiddleware',
'uds.core.util.middleware.xua.XUACompatibleMiddleware',
'uds.core.util.middleware.redirect.RedirectMiddleware',
]
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

View File

@ -35,5 +35,3 @@ take care of registering it as provider
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from .authenticator import RadiusAuth
__updated__ = '2014-02-19'

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013-2020 Virtual Cable S.L.U.
# Copyright (c) 2013-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -25,70 +25,3 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
from django.urls import reverse
from django.http import HttpResponseRedirect
from uds.core.util.config import GlobalConfig
logger = logging.getLogger(__name__)
class XUACompatibleMiddleware:
"""
Add a X-UA-Compatible header to the response
This header tells to Internet Explorer to render page with latest
possible version or to use chrome frame if it is installed.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if response.get('content-type', '').startswith('text/html'):
response['X-UA-Compatible'] = 'IE=edge'
return response
class RedirectMiddleware:
"""
This class is responsible of redirection, if checked, requests to HTTPS.
Some paths will not be redirected, to avoid problems, but they are advised to use SSL (this is for backwards compat)
"""
NO_REDIRECT = [
'rest',
'pam',
'guacamole',
# For new paths
# 'uds/rest', # REST must be HTTPS if redirect is enabled
'uds/pam',
'uds/guacamole',
'uds/rest/client/test'
]
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
full_path = request.get_full_path()
redirect = True
for nr in RedirectMiddleware.NO_REDIRECT:
if full_path.startswith('/' + nr):
redirect = False
break
if redirect and request.is_secure() is False and GlobalConfig.REDIRECT_TO_HTTPS.getBool():
if request.method == 'POST':
# url = request.build_absolute_uri(GlobalConfig.LOGIN_URL.get())
url = reverse('page.login')
else:
url = request.build_absolute_uri(full_path)
url = url.replace('http://', 'https://')
return HttpResponseRedirect(url)
return self.get_response(request)
@staticmethod
def registerException(path: str) -> None:
RedirectMiddleware.NO_REDIRECT.append(path)

View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
from django.urls import reverse
from django.http import HttpResponseRedirect
from uds.core.util.config import GlobalConfig
logger = logging.getLogger(__name__)
class RedirectMiddleware:
"""
This class is responsible of redirection, if checked, requests to HTTPS.
Some paths will not be redirected, to avoid problems, but they are advised to use SSL (this is for backwards compat)
"""
NO_REDIRECT = [
'rest',
'pam',
'guacamole',
# For new paths
# 'uds/rest', # REST must be HTTPS if redirect is enabled
'uds/pam',
'uds/guacamole',
'uds/rest/client/test'
]
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
full_path = request.get_full_path()
redirect = True
for nr in RedirectMiddleware.NO_REDIRECT:
if full_path.startswith('/' + nr):
redirect = False
break
if redirect and request.is_secure() is False and GlobalConfig.REDIRECT_TO_HTTPS.getBool():
if request.method == 'POST':
# url = request.build_absolute_uri(GlobalConfig.LOGIN_URL.get())
url = reverse('page.login')
else:
url = request.build_absolute_uri(full_path)
url = url.replace('http://', 'https://')
return HttpResponseRedirect(url)
return self.get_response(request)
@staticmethod
def registerException(path: str) -> None:
RedirectMiddleware.NO_REDIRECT.append(path)

View File

@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import threading
import datetime
import weakref
import logging
import typing
from django.http import HttpRequest, HttpResponse
from django.utils import timezone
from uds.core.util import os_detector as OsDetector
from uds.core.util.config import GlobalConfig
from uds.core.auths.auth import EXPIRY_KEY, ROOT_ID, USER_KEY, getRootUser, webLogout
from uds.core.util.request import setRequest, delCurrentRequest, cleanOldRequests, ExtendedHttpRequest
from uds.models import User
logger = logging.getLogger(__name__)
# How often to check the requests cache for stuck objects
CHECK_SECONDS = 3600 * 24 # Once a day is more than enough
class GlobalRequestMiddleware:
lastCheck: typing.ClassVar[datetime.datetime] = datetime.datetime.now()
def __init__(self, get_response: typing.Callable[[HttpRequest], HttpResponse]):
self._get_response: typing.Callable[[HttpRequest], HttpResponse] = get_response
def _process_request(self, request: ExtendedHttpRequest) -> None:
# Store request on cache
setRequest(request=request)
# Add IP to request
GlobalRequestMiddleware.fillIps(request)
# Ensures request contains os
request.os = OsDetector.getOsFromUA(
request.META.get('HTTP_USER_AGENT', 'Unknown')
)
# Ensures that requests contains the valid user
GlobalRequestMiddleware.getUser(request)
def _process_response(self, request: ExtendedHttpRequest, response: HttpResponse):
# Remove IP from global cache (processing responses after this will make global request unavailable,
# but can be got from request again)
delCurrentRequest()
# Clean old stored if needed
GlobalRequestMiddleware.cleanStuckRequests()
return response
def __call__(self, request: ExtendedHttpRequest):
self._process_request(request)
# Now, check if session is timed out...
if request.user:
# return HttpResponse(content='Session Expired', status=403, content_type='text/plain')
now = timezone.now()
expiry = request.session.get(EXPIRY_KEY, now)
if expiry < now:
webLogout(
request=request
) # Ignore the response, just processes usere session logout
return HttpResponse(content='Session Expired', status=403)
# Update session timeout..self.
request.session[EXPIRY_KEY] = now + datetime.timedelta(
seconds=GlobalConfig.SESSION_DURATION_ADMIN.getInt()
if request.user.isStaff()
else GlobalConfig.SESSION_DURATION_USER.getInt()
)
response = self._get_response(request)
return self._process_response(request, response)
@staticmethod
def cleanStuckRequests() -> None:
# In case of some exception, keep clean very old request from time to time...
if (
GlobalRequestMiddleware.lastCheck
> datetime.datetime.now() - datetime.timedelta(seconds=CHECK_SECONDS)
):
return
cleanOldRequests()
@staticmethod
def fillIps(request: ExtendedHttpRequest):
"""
Obtains the IP of a Django Request, even behind a proxy
Returns the obtained IP, that always will be a valid ip address.
"""
behind_proxy = GlobalConfig.BEHIND_PROXY.getBool(False)
try:
request.ip = request.META['REMOTE_ADDR']
except Exception:
logger.exception('Request ip not found!!')
request.ip = '' # No remote addr?? ...
try:
proxies = request.META['HTTP_X_FORWARDED_FOR'].split(",")
request.ip_proxy = proxies[0]
if not request.ip or behind_proxy:
# Request.IP will be None in case of nginx & gunicorn
# Some load balancers may include "domains" on x-forwarded for,
request.ip = request.ip_proxy.split('%')[0] # Stores the ip
# will raise "list out of range", leaving ip_proxy = proxy in case of no other proxy apart of nginx
request.ip_proxy = proxies[1].strip()
except Exception:
request.ip_proxy = request.ip
@staticmethod
def getUser(request: ExtendedHttpRequest) -> None:
"""
Ensures request user is the correct user
"""
user_id = request.session.get(USER_KEY)
user: typing.Optional[User] = None
if user_id:
try:
if user_id == ROOT_ID:
user = getRootUser()
else:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
user = None
logger.debug('User at Middleware: %s %s', user_id, user)
request.user = user

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
from django.urls import reverse
from django.http import HttpResponseRedirect
from uds.core.util.config import GlobalConfig
logger = logging.getLogger(__name__)
class XUACompatibleMiddleware:
"""
Add a X-UA-Compatible header to the response
This header tells to Internet Explorer to render page with latest
possible version or to use chrome frame if it is installed.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if response.get('content-type', '').startswith('text/html'):
response['X-UA-Compatible'] = 'IE=edge'
return response

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@ -35,17 +35,14 @@ import weakref
import logging
import typing
from django.http import HttpRequest, HttpResponse
from django.utils import timezone
from django.http import HttpRequest
from uds.core.util import os_detector as OsDetector
from uds.core.util.tools import DictAsObj
from uds.core.util.config import GlobalConfig
from uds.core.auths.auth import EXPIRY_KEY, ROOT_ID, USER_KEY, getRootUser, webLogout
from uds.models import User
# How often to check the requests cache for stuck objects
CHECK_SECONDS = 3600 * 24 # Once a day is more than enough
logger = logging.getLogger(__name__)
_requests: typing.Dict[int, typing.Tuple[weakref.ref, datetime.datetime]] = {}
class ExtendedHttpRequest(HttpRequest):
@ -59,11 +56,6 @@ class ExtendedHttpRequestWithUser(ExtendedHttpRequest):
user: User
logger = logging.getLogger(__name__)
_requests: typing.Dict[int, typing.Tuple[weakref.ref, datetime.datetime]] = {}
def getIdent() -> int:
ident = threading.current_thread().ident
return ident if ident else -1
@ -80,144 +72,36 @@ def getRequest() -> ExtendedHttpRequest:
return ExtendedHttpRequest()
def delCurrentRequest() -> None:
ident = getIdent()
logger.debug('Deleting %s', ident)
try:
if ident in _requests:
del _requests[ident] # Remove stored request
else:
logger.info('Request id %s not stored in cache', ident)
except Exception:
logger.exception('Deleting stored request')
class UDSSessionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
class GlobalRequestMiddleware:
lastCheck: typing.ClassVar[datetime.datetime] = datetime.datetime.now()
def __init__(self, get_response: typing.Callable[[HttpRequest], HttpResponse]):
self._get_response: typing.Callable[[HttpRequest], HttpResponse] = get_response
def _process_request(self, request: ExtendedHttpRequest) -> None:
# Store request on cache
_requests[getIdent()] = (
weakref.ref(typing.cast(ExtendedHttpRequest, request)),
datetime.datetime.now(),
)
# Add IP to request
GlobalRequestMiddleware.fillIps(request)
# Ensures request contains os
request.os = OsDetector.getOsFromUA(
request.META.get('HTTP_USER_AGENT', 'Unknown')
)
# Ensures that requests contains the valid user
GlobalRequestMiddleware.getUser(request)
def _process_response(self, request: ExtendedHttpRequest, response: HttpResponse):
# Remove IP from global cache (processing responses after this will make global request unavailable,
# but can be got from request again)
ident = getIdent()
logger.debug('Deleting %s', ident)
def cleanOldRequests() -> None:
logger.debug('Cleaning stuck requests from %s', _requests)
# No request lives 60 seconds, so 60 seconds is fine
cleanFrom: datetime.datetime = datetime.datetime.now() - datetime.timedelta(
seconds=60
)
toDelete: typing.List[int] = []
for ident, request in _requests.items():
if request[1] < cleanFrom:
toDelete.append(ident)
for ident in toDelete:
try:
if ident in _requests:
del _requests[ident] # Remove stored request
else:
logger.info('Request id %s not stored in cache', ident)
del _requests[ident]
except Exception:
logger.exception('Deleting stored request')
pass # Ignore it silently
# Clean old stored if needed
GlobalRequestMiddleware.cleanStuckRequests()
return response
def __call__(self, request: ExtendedHttpRequest):
self._process_request(request)
# Now, check if session is timed out...
if request.user:
# return HttpResponse(content='Session Expired', status=403, content_type='text/plain')
now = timezone.now()
expiry = request.session.get(EXPIRY_KEY, now)
if expiry < now:
webLogout(
request=request
) # Ignore the response, just processes usere session logout
return HttpResponse(content='Session Expired', status=403)
# Update session timeout..self.
request.session[EXPIRY_KEY] = now + datetime.timedelta(
seconds=GlobalConfig.SESSION_DURATION_ADMIN.getInt()
if request.user.isStaff()
else GlobalConfig.SESSION_DURATION_USER.getInt()
)
response = self._get_response(request)
return self._process_response(request, response)
@staticmethod
def cleanStuckRequests() -> None:
# In case of some exception, keep clean very old request from time to time...
if (
GlobalRequestMiddleware.lastCheck
> datetime.datetime.now() - datetime.timedelta(seconds=CHECK_SECONDS)
):
return
logger.debug('Cleaning stuck requests from %s', _requests)
# No request lives 60 seconds, so 60 seconds is fine
cleanFrom: datetime.datetime = datetime.datetime.now() - datetime.timedelta(
seconds=60
)
toDelete: typing.List[int] = []
for ident, request in _requests.items():
if request[1] < cleanFrom:
toDelete.append(ident)
for ident in toDelete:
try:
del _requests[ident]
except Exception:
pass # Ignore it silently
@staticmethod
def fillIps(request: ExtendedHttpRequest):
"""
Obtains the IP of a Django Request, even behind a proxy
Returns the obtained IP, that always will be a valid ip address.
"""
behind_proxy = GlobalConfig.BEHIND_PROXY.getBool(False)
try:
request.ip = request.META['REMOTE_ADDR']
except Exception:
logger.exception('Request ip not found!!')
request.ip = '' # No remote addr?? ...
try:
proxies = request.META['HTTP_X_FORWARDED_FOR'].split(",")
request.ip_proxy = proxies[0]
if not request.ip or behind_proxy:
# Request.IP will be None in case of nginx & gunicorn
# Some load balancers may include "domains" on x-forwarded for,
request.ip = request.ip_proxy.split('%')[0] # Stores the ip
# will raise "list out of range", leaving ip_proxy = proxy in case of no other proxy apart of nginx
request.ip_proxy = proxies[1].strip()
except Exception:
request.ip_proxy = request.ip
@staticmethod
def getUser(request: ExtendedHttpRequest) -> None:
"""
Ensures request user is the correct user
"""
user_id = request.session.get(USER_KEY)
user: typing.Optional[User] = None
if user_id:
try:
if user_id == ROOT_ID:
user = getRootUser()
else:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
user = None
logger.debug('User at Middleware: %s %s', user_id, user)
request.user = user
def setRequest(request: ExtendedHttpRequest):
_requests[getIdent()] = (
weakref.ref(typing.cast(ExtendedHttpRequest, request)),
datetime.datetime.now(),
)