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:
parent
e325653019
commit
e292e726d3
server/src
tests
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
|
||||
"""
|
||||
|
30
server/src/tests/core/util/middleware/__init__.py
Normal file
30
server/src/tests/core/util/middleware/__init__.py
Normal file
@ -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
|
||||
|
101
server/src/uds/core/util/middleware/builder.py
Normal file
101
server/src/uds/core/util/middleware/builder.py
Normal file
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user