1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-31 22:50:32 +03:00

Async fixes

Async client for UDS tests included
refactorized UDS middelwares to be sync and async
This commit is contained in:
Adolfo Gómez García 2022-12-08 14:44:47 +01:00
parent e325653019
commit e292e726d3
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
8 changed files with 393 additions and 225 deletions
server/src
tests
core/util
utils
uds/core/util/middleware

@ -26,5 +26,5 @@
# 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.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.
# 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.
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""

@ -32,7 +32,7 @@ import typing
import logging
from django.test import TestCase, TransactionTestCase
from django.test.client import Client
from django.test.client import Client, AsyncClient # type: ignore # Pylance does not know about AsyncClient, but it is there
from django.http.response import HttpResponse
from django.conf import settings
@ -55,17 +55,12 @@ class UDSHttpResponse(HttpResponse):
return super().json() # type: ignore
class UDSClient(Client):
class UDSClientMixin:
headers: typing.Dict[str, str] = {
'HTTP_USER_AGENT': 'Testing user agent',
}
def __init__(
self,
enforce_csrf_checks: bool = False,
raise_request_exception: bool = True,
**defaults: typing.Any
):
def initialize(self):
# Ensure only basic middleware are enabled.
settings.MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
@ -80,15 +75,30 @@ class UDSClient(Client):
settings.RSA_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcANi/08cnpn04\njKW/o2G1k4SIa6dJks8DmT4MQHOWqYC46YSIIPzqGoBPcvkbDSPSFBnByo3HhMY+\nk4JHc9SUwEmHSJWDCHjt7XSXX/ryqH0QQIJtSjk9Bc+GkOU24mnbITiw7ORjp7VN\nvgFdFhjVsZM/NjX/6Y9DoCPC1mGj0O9Dd4MfCsNwUxRhhR6LdrEnpRUSVW0Ksxbz\ncTfpQjdFr86+1BeUbzqN2HDcEGhvioj+0lGPXcOZoRNYU16H7kjLNP+o+rC7f/q/\nfoOYLzDSkmzePbcG+g0Hv7K7fuLus05ZWjupOmJA9hytB1BIF4p5f4ewl05Fx2Zj\nG2LneO2fAgMBAAECggEBANDimOnh2TkDceeMWx+OsAooC3E/zbEkjBudl3UoiNcn\nD0oCpkxeDeT0zpkgz/ZoTnd7kE0Y1e73WQc3JT5UcyXdQLMLLrIgDDnT+Jx1jB5z\n7XLN3UiJbblL2BOrZYbsCJf/fgU2l08rgBBVdJP+lAvps6YUAcd+6gDKfsnSpRhU\nWBHLZde7l6vUJ2OK9ZmHaghF5E8Xx918OSUKFJfGTYL5JLTb/scdl8vQse1quWC1\nk48PPXK10vOFvYWonQpRb2cOK/PPjPXPNWzcQyQY9D1iOeFvRyLqOXYE/ZY+qDe2\nHdPGrkl67yz01nzepkWWg/ZNbMXeZZyOnZm0aXtOxtkCgYEA/Qz3mescgwrt67yh\nFrbXjUqiVf2IpbNt88CUcbY0r1EdTA9OMtOtPYNvfpyRIRfDaZJ1zAdh3CZ2/hTm\ng+VUtseKnUDCi0xIBKX3V2O8sryWt2KStTnTo6JP0T47yXvmaRu5cutgoaD9SK+r\nN5vg1D2gNLmsT8uJh1Bl/yWGC4sCgYEA3pFGgAmiywsvmsddkI+LujoQVTiqkfFg\nMHHsJFOZlhYO83g49Q11pcQ70ukT6e89Ggwy///+z19p8jJ+wGqQWQLsM6eO1utg\nnJ8wMTwk8tOEm9MnWnnWhtG9KWcgkmwOVQiesJdWa1xOqsBKGchUkugmFycKNsiG\nHUbogbJ0OL0CgYBVLIcuxKdNKGGaxlwGVDbLdQKdJQBYncN1ly2f9K9ZD1loH4K3\nsu4N1W6y1Co5VFFO+KAzs4xp2HyW2xwX6xoPh6yNb53L2zombmKJhKWgF8A3K7Or\n0jH9UwXArUzcbZrJaC6MktNss85tJ8vepNYROkjxVkm8dgrtg89BCTVMLwKBgQCW\nSSh+uoL3cdUyQV63h4ZFOIHg2cOrin52F+bpXJ3/z2NHGa30IqOHTGtM7l+o/geX\nOBeT72tC4d2rUlduXEaeJDAUbRcxnnx9JayoAkG8ygDoK3uOR2kJXkTJ2T4QQPCo\nkIp/GaGcGxdviyo+IJyjGijmR1FJTrvotwG22iZKTQKBgQCIh50Dz0/rqZB4Om5g\nLLdZn1C8/lOR8hdK9WUyPHZfJKpQaDOlNdiy9x6xD6+uIQlbNsJhlDbOudHDurfI\nghGbJ1sy1FUloP+V3JAFS88zIwrddcGEso8YMFMCE1fH2/q35XGwZEnUq7ttDaxx\nHmTQ2w37WASIUgCl2GhM25np0Q==\n-----END PRIVATE KEY-----\n'
settings.CERTIFICATE = '-----BEGIN CERTIFICATE-----\nMIICzTCCAjYCCQCOUQEWpuEa3jANBgkqhkiG9w0BAQUFADCBqjELMAkGA1UEBhMC\nRVMxDzANBgNVBAgMBk1hZHJpZDEUMBIGA1UEBwwLQWxjb3Jjw4PCs24xHTAbBgNV\nBAoMFFZpcnR1YWwgQ2FibGUgUy5MLlUuMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEY\nMBYGA1UEAwwPQWRvbGZvIEfDg8KzbWV6MSUwIwYJKoZIhvcNAQkBFhZhZ29tZXpA\ndmlydHVhbGNhYmxlLmVzMB4XDTEyMDYyNTA0MjM0MloXDTEzMDYyNTA0MjM0Mlow\ngaoxCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxFDASBgNVBAcMC0FsY29y\nY8ODwrNuMR0wGwYDVQQKDBRWaXJ0dWFsIENhYmxlIFMuTC5VLjEUMBIGA1UECwwL\nRGV2ZWxvcG1lbnQxGDAWBgNVBAMMD0Fkb2xmbyBHw4PCs21lejElMCMGCSqGSIb3\nDQEJARYWYWdvbWV6QHZpcnR1YWxjYWJsZS5lczCBnzANBgkqhkiG9w0BAQEFAAOB\njQAwgYkCgYEA35iGyHS/GVdWk3n9kQ+wsCLR++jd9Vez/s407/natm8YDteKksA0\nMwIvDAX722blm8PUya2NOlnum8KdyUPDOq825XERDlsIA+sTd6lb1c7w44qZ/pb+\n68mhXoRx2VJsu//+zhBkaQ1/KcugeHa4WLRIH35YLxdQDxrXS1eQWccCAwEAATAN\nBgkqhkiG9w0BAQUFAAOBgQAk+fJPpY+XvUsxR2A4SaQ8TGnE2x4PtpwCrCVzKEU9\nW2ugdXvysxkHbib3+JdA6s+lJjHs5HiMZPo/ak8adEKke+d10EU5YcUaJRRUpStY\nqQHziaqOl5Hgi75Kjskq6+tCU0Iui+s9pBg0V6y1AQsCmH2xFs7t1oEOGRFVarfF\n4Q==\n-----END CERTIFICATE-----'
def add_header(self, name: str, value: str):
self.headers[name] = value
# Use "BEFORE" first client use
def add_middelware(self, middleware: str) -> None:
if middleware not in settings.MIDDLEWARE:
settings.MIDDLEWARE.append(middleware)
class UDSClient(UDSClientMixin, Client):
def __init__(
self,
enforce_csrf_checks: bool = False,
raise_request_exception: bool = True,
**defaults: typing.Any
):
UDSClientMixin.initialize(self)
# Instantiate the client and add basic user agent
super().__init__(enforce_csrf_checks, raise_request_exception)
# and required UDS cookie
self.cookies['uds'] = CryptoManager().randomString(48)
def add_header(self, name: str, value: str):
self.headers[name] = value
def request(self, **request: typing.Any):
# Copy request dict
request = request.copy()
@ -102,26 +112,65 @@ class UDSClient(Client):
def post(self, *args, **kwargs) -> 'UDSHttpResponse':
return typing.cast('UDSHttpResponse', super().post(*args, **kwargs))
class UDSAsyncClient(UDSClientMixin, AsyncClient):
def __init__(
self,
enforce_csrf_checks: bool = False,
raise_request_exception: bool = True,
**defaults: typing.Any
):
UDSClientMixin.initialize(self)
# Instantiate the client and add basic user agent
super().__init__(enforce_csrf_checks, raise_request_exception) # type: ignore # Coplains, but this is ok
# and required UDS cookie
self.cookies['uds'] = CryptoManager().randomString(48)
async def request(self, **request: typing.Any):
# Copy request dict
request = request.copy()
# Add headers
request.update(self.headers)
return await super().request(**request)
async def get(self, *args, **kwargs) -> 'UDSHttpResponse':
return typing.cast('UDSHttpResponse', await super().get(*args, **kwargs))
async def post(self, *args, **kwargs) -> 'UDSHttpResponse':
return typing.cast('UDSHttpResponse', await super().post(*args, **kwargs))
class UDSTestCase(TestCase):
client_class: typing.Type = UDSClient
async_client_class: typing.Type = UDSAsyncClient
client: UDSClient
async_client: UDSAsyncClient
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
setupClass(cls)
setupClass(cls) # The one local to this module
class UDSTransactionTestCase(TransactionTestCase):
client_class: typing.Type = UDSClient
async_client_class: typing.Type = UDSAsyncClient
client: UDSClient
async_client: UDSAsyncClient
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
setupClass(cls)
def setupClass(cls: typing.Union[typing.Type[UDSTestCase], typing.Type[UDSTransactionTestCase]]) -> None:
def setupClass(
cls: typing.Union[typing.Type[UDSTestCase], typing.Type[UDSTransactionTestCase]]
) -> None:
# Nothing right now
pass

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 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.
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import datetime
import logging
import typing
import asyncio
from django.utils.decorators import sync_and_async_middleware
from django.http import HttpResponseForbidden
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 (
AUTHORIZED_KEY,
EXPIRY_KEY,
ROOT_ID,
USER_KEY,
getRootUser,
webLogout,
)
from uds.models import User
if typing.TYPE_CHECKING:
from django.http import HttpResponse
from uds.core.util.request import ExtendedHttpRequest
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
RequestMiddelwareProcessorType = typing.Callable[
['ExtendedHttpRequest'], typing.Optional['HttpResponse']
]
ResponseMiddelwareProcessorType = typing.Callable[
['ExtendedHttpRequest', 'HttpResponse'], 'HttpResponse'
]
def build_middleware(
request_processor: RequestMiddelwareProcessorType,
response_processor: ResponseMiddelwareProcessorType,
) -> typing.Callable[[typing.Any], typing.Union[typing.Callable, typing.Coroutine]]:
"""
Creates a method to be used as a middleware, synchronously or asynchronously
"""
@sync_and_async_middleware
def middleware(
get_response: typing.Any,
) -> typing.Union[typing.Callable, typing.Coroutine]:
if asyncio.iscoroutinefunction(get_response):
async def async_middleware(
request: 'ExtendedHttpRequest',
) -> 'HttpResponse':
response = request_processor(request)
return response_processor(
request, response or await get_response(request)
)
return async_middleware
else:
def sync_middleware(request: 'ExtendedHttpRequest') -> 'HttpResponse':
response = request_processor(request)
return response_processor(request, response or get_response(request))
return sync_middleware
return middleware

@ -25,16 +25,20 @@
# 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 asyncio
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
from django.urls import reverse
from django.http import HttpResponseRedirect
from django.utils.decorators import sync_and_async_middleware
from uds.core.util.config import GlobalConfig
from . import builder
logger = logging.getLogger(__name__)
if typing.TYPE_CHECKING:
@ -55,42 +59,34 @@ _NO_REDIRECT: typing.List[str] = [
'uds/rest/tunnel',
]
def registerException(path: str) -> None:
_NO_REDIRECT.append(path)
@sync_and_async_middleware
def RedirectMiddleware(get_response: typing.Any) -> typing.Union[typing.Callable, typing.Coroutine]:
def check_redirectable(request: 'HttpRequest') -> typing.Optional['HttpResponse']:
full_path = request.get_full_path()
redirect = True
for nr in _NO_REDIRECT:
if full_path.startswith('/' + nr):
redirect = False
break
if (
redirect
and not request.is_secure()
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://')
def _check_redirectable(request: 'HttpRequest') -> typing.Optional['HttpResponse']:
full_path = request.get_full_path()
redirect = True
for nr in _NO_REDIRECT:
if full_path.startswith('/' + nr):
redirect = False
break
return HttpResponseRedirect(url)
return None
if (
redirect
and not request.is_secure()
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://')
# One-time configuration and initialization.
if asyncio.iscoroutinefunction(get_response):
async def async_middleware(request: 'HttpRequest') -> 'HttpResponse':
response = check_redirectable(request)
return response or await get_response(request)
return async_middleware
else:
def sync_middleware(request: 'HttpRequest') -> 'HttpResponse':
response = check_redirectable(request)
return response or get_response(request)
return sync_middleware
return HttpResponseRedirect(url)
return None
# Compatibility with old middleware, so we can use it in settings.py as it was
RedirectMiddleware = builder.build_middleware(_check_redirectable, lambda _, y: y)

@ -26,13 +26,13 @@
# 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
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import datetime
import logging
import typing
from django.http import HttpRequest, HttpResponse
from django.http import HttpResponseForbidden
from django.utils import timezone
from uds.core.util import os_detector as OsDetector
@ -47,7 +47,10 @@ from uds.core.auths.auth import (
)
from uds.models import User
from . import builder
if typing.TYPE_CHECKING:
from django.http import HttpResponse
from uds.core.util.request import ExtendedHttpRequest
@ -57,132 +60,123 @@ logger = logging.getLogger(__name__)
CHECK_SECONDS = 3600 * 24 # Once a day is more than enough
class GlobalRequestMiddleware:
__slots__ = ('_get_response',)
def _fill_ips(request: 'ExtendedHttpRequest') -> None:
"""
Obtains the IP of a Django Request, even behind a proxy
lastCheck: typing.ClassVar[datetime.datetime] = datetime.datetime.now()
Returns the obtained IP, that always will be a valid ip address.
"""
behind_proxy = GlobalConfig.BEHIND_PROXY.getBool(False)
try:
request.ip = request.META.get('REMOTE_ADDR', '')
except Exception:
request.ip = '' # No remote addr?? ...
def __init__(self, get_response: typing.Callable[[HttpRequest], HttpResponse]):
self._get_response: typing.Callable[[HttpRequest], HttpResponse] = get_response
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)
return response
def __call__(self, request: 'ExtendedHttpRequest'):
# Add IP to request
GlobalRequestMiddleware.fillIps(request)
request.authorized = request.session.get(AUTHORIZED_KEY, False)
# Ensures request contains os
request.os = OsDetector.getOsFromUA(
request.META.get('HTTP_USER_AGENT', 'Unknown')
# X-FORWARDED-FOR: CLIENT, FAR_PROXY, PROXY, NEAR_PROXY, NGINX
# We will accept only 2 proxies, the last ones
proxies = list(
reversed(
[
i.split('%')[0]
for i in request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")
]
)
)
# proxies = list(reversed(['172.27.0.8', '172.27.0.128', '172.27.0.1']))
# proxies = list(reversed(['172.27.0.12', '172.27.0.1']))
# proxies = list(reversed(['172.27.0.12']))
# request.ip = ''
# Ensures that requests contains the valid user
GlobalRequestMiddleware.getUser(request)
logger.debug('Detected proxies: %s', proxies)
# Now, check if session is timed out...
if request.user:
# return HttpResponse(content='Session Expired', status=403, content_type='text/plain')
now = timezone.now()
try:
expiry = datetime.datetime.fromisoformat(
request.session.get(EXPIRY_KEY, '')
)
except ValueError:
expiry = now
if expiry < now:
try:
return webLogout(request=request)
except Exception: # nosec: intentionaly catching all exceptions and ignoring them
pass # If fails, we don't care, we just want to 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()
)
).isoformat() # store as ISO format, str, json serilizable
# Request.IP will be None in case of nginx & gunicorn using sockets, as we do
if not request.ip:
request.ip = proxies[0] # Stores the ip
proxies = proxies[1:] # Remove from proxies list
response = self._get_response(request)
logger.debug('Proxies: %s', proxies)
# Update authorized on session
if hasattr(request, 'session'):
request.session[AUTHORIZED_KEY] = request.authorized
request.ip_proxy = proxies[0] if proxies and proxies[0] else request.ip
return self._process_response(request, response)
if behind_proxy:
request.ip = request.ip_proxy
request.ip_proxy = proxies[1] if len(proxies) > 1 else request.ip
logger.debug('Behind a proxy is active')
@staticmethod
def fillIps(request: 'ExtendedHttpRequest'):
"""
Obtains the IP of a Django Request, even behind a proxy
# Check if ip are ipv6 and set version field
request.ip_version = 6 if ':' in request.ip else 4
Returns the obtained IP, that always will be a valid ip address.
"""
behind_proxy = GlobalConfig.BEHIND_PROXY.getBool(False)
logger.debug('ip: %s, ip_proxy: %s', request.ip, request.ip_proxy)
def _get_user(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:
request.ip = request.META.get('REMOTE_ADDR', '')
except Exception:
request.ip = '' # No remote addr?? ...
if user_id == ROOT_ID:
user = getRootUser()
else:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
user = None
# X-FORWARDED-FOR: CLIENT, FAR_PROXY, PROXY, NEAR_PROXY, NGINX
# We will accept only 2 proxies, the last ones
proxies = list(
reversed(
[
i.split('%')[0]
for i in request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")
]
logger.debug('User at Middleware: %s %s', user_id, user)
request.user = user
def _process_request(request: 'ExtendedHttpRequest') -> typing.Optional['HttpResponse']:
# Add IP to request, user, ...
# Add IP to request
_fill_ips(request)
request.authorized = request.session.get(AUTHORIZED_KEY, False)
# Ensures request contains os
request.os = OsDetector.getOsFromUA(request.META.get('HTTP_USER_AGENT', 'Unknown'))
# Ensures that requests contains the valid user
_get_user(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()
try:
expiry = datetime.datetime.fromisoformat(
request.session.get(EXPIRY_KEY, '')
)
)
# proxies = list(reversed(['172.27.0.8', '172.27.0.128', '172.27.0.1']))
# proxies = list(reversed(['172.27.0.12', '172.27.0.1']))
# proxies = list(reversed(['172.27.0.12']))
# request.ip = ''
logger.debug('Detected proxies: %s', proxies)
# Request.IP will be None in case of nginx & gunicorn using sockets, as we do
if not request.ip:
request.ip = proxies[0] # Stores the ip
proxies = proxies[1:] # Remove from proxies list
logger.debug('Proxies: %s', proxies)
request.ip_proxy = proxies[0] if proxies and proxies[0] else request.ip
if behind_proxy:
request.ip = request.ip_proxy
request.ip_proxy = proxies[1] if len(proxies) > 1 else request.ip
logger.debug('Behind a proxy is active')
# Check if ip are ipv6 and set version field
request.ip_version = 6 if ':' in request.ip else 4
logger.debug('ip: %s, ip_proxy: %s', request.ip, request.ip_proxy)
@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:
except ValueError:
expiry = now
if expiry < now:
try:
if user_id == ROOT_ID:
user = getRootUser()
else:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
user = None
return webLogout(request=request)
except Exception: # nosec: intentionaly catching all exceptions and ignoring them
pass # If fails, we don't care, we just want to logout
return HttpResponseForbidden(content='Session Expired', content_type='text/plain')
# 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()
)
).isoformat() # store as ISO format, str, json serilizable
logger.debug('User at Middleware: %s %s', user_id, user)
return None
request.user = user
def _process_response(
request: 'ExtendedHttpRequest', response: 'HttpResponse'
) -> 'HttpResponse':
# Update authorized on session
if hasattr(request, 'session'):
request.session[AUTHORIZED_KEY] = request.authorized
return response
# Compatibility with old middleware, so we can use it in settings.py as it was
GlobalRequestMiddleware = builder.build_middleware(_process_request, _process_response)

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021-2022 Virtual Cable S.L.U.
# Copyright (c) 2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -25,65 +25,63 @@
# 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.
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import re
import logging
import typing
logger = logging.getLogger(__name__)
from django.http import HttpResponse
from django.http import HttpResponseForbidden
from uds.core.util.config import GlobalConfig
from uds.core.auths.auth import isTrustedSource
from uds.core.util.request import ExtendedHttpRequest
from . import builder
if typing.TYPE_CHECKING:
from django.http import HttpRequest
from django.http import HttpResponse
from uds.core.util.request import ExtendedHttpRequest
# Simple Bot detection
bot = re.compile(r'bot|spider', re.IGNORECASE)
class UDSSecurityMiddleware:
'''
This class contains all the security checks done by UDS in order to add some extra protection.
'''
__slots__ = ('get_response',)
get_response: typing.Any # typing.Callable[['HttpRequest'], 'HttpResponse']
def __init__(
self, get_response: typing.Callable[['HttpRequest'], 'HttpResponse']
) -> None:
self.get_response = get_response
def __call__(self, request: 'ExtendedHttpRequest') -> 'HttpResponse':
ua = request.META.get(
'HTTP_USER_AGENT', 'Unknown'
def _process_request(request: 'ExtendedHttpRequest') -> typing.Optional['HttpResponse']:
ua = request.META.get('HTTP_USER_AGENT', 'Unknown')
# If bot, break now
if bot.search(ua) or (ua == 'Unknown' and not isTrustedSource(request.ip)):
# Return emty response if bot is detected
logger.info(
'Denied Bot %s from %s to %s',
ua,
request.META.get(
'REMOTE_ADDR',
request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")[-1],
),
request.path,
)
# If bot, break now
if bot.search(ua) or (ua == 'Unknown' and not isTrustedSource(request.ip)):
# Return emty response if bot is detected
logger.info(
'Denied Bot %s from %s to %s',
ua,
request.META.get(
'REMOTE_ADDR',
request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")[-1],
),
request.path,
)
return HttpResponse(content='Forbbiden', status=403)
return HttpResponseForbidden(content='Forbbiden', content_type='text/plain')
return None
response = self.get_response(request)
if GlobalConfig.ENHANCED_SECURITY.getBool():
# Legacy browser support for X-XSS-Protection
response.headers.setdefault('X-XSS-Protection', '1; mode=block')
# Add Content-Security-Policy, see https://www.owasp.org/index.php/Content_Security_Policy
response.headers.setdefault(
'Content-Security-Policy',
"default-src 'self' 'unsafe-inline' 'unsafe-eval' uds: udss:; img-src 'self' https: data:;",
)
return response
def _process_response(
request: 'ExtendedHttpRequest', response: 'HttpResponse'
) -> 'HttpResponse':
if GlobalConfig.ENHANCED_SECURITY.getBool():
# Legacy browser support for X-XSS-Protection
response.headers.setdefault('X-XSS-Protection', '1; mode=block') # type: ignore
# Add Content-Security-Policy, see https://www.owasp.org/index.php/Content_Security_Policy
response.headers.setdefault( # type: ignore
'Content-Security-Policy',
"default-src 'self' 'unsafe-inline' 'unsafe-eval' uds: udss:; img-src 'self' https: data:;",
)
return response
# Compatibility with old middleware, so we can use it in settings.py as it was
UDSSecurityMiddleware = builder.build_middleware(_process_request, _process_response)

@ -26,27 +26,27 @@
# 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
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import logging
from . import builder
if typing.TYPE_CHECKING:
from django.http import HttpResponse
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
def _process_response(
request: 'ExtendedHttpRequest', response: 'HttpResponse'
) -> 'HttpResponse':
if response.get('content-type', '').startswith('text/html'):
response['X-UA-Compatible'] = 'IE=edge'
return response
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.
"""
__slots__ = ('get_response',)
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
# 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. TO BE REMOVED SOON (edge does not need it)
XUACompatibleMiddleware = builder.build_middleware(lambda x: None, _process_response)