1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-23 17:34:17 +03:00

Done basic server events REST methods

This commit is contained in:
Adolfo Gómez García 2023-09-07 22:56:54 +02:00
parent e96369bd71
commit d9b7771f21
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
12 changed files with 248 additions and 76 deletions

View File

@ -64,6 +64,15 @@ class ServerEventsLoginLogoutTest(rest.test.RESTTestCase):
# 'user_service': 'uuid', # MUST BE PRESENT
# 'username': 'username', # Optional
# }
# Returns:
#
# {
# 'ip': src.ip,
# 'hostname': src.hostname,
# 'dead_line': deadLine,
# 'max_idle': maxIdle,
# 'session_id': session_id,
# }
response = self.client.rest_post(
'/servers/event',
data={
@ -76,6 +85,14 @@ class ServerEventsLoginLogoutTest(rest.test.RESTTestCase):
self.assertEqual(response.status_code, 200)
self.user_service_managed.refresh_from_db()
self.assertEqual(self.user_service_managed.in_use, True)
result = response.json()['result']
self.assertEqual(self.user_service_managed.src_ip, result['ip'])
self.assertEqual(self.user_service_managed.src_hostname, result['hostname'])
session = self.user_service_managed.sessions.first()
if session is None:
self.fail('Session not found')
self.assertEqual(session.session_id, result['session_id'])
self.assertEqual(self.user_service_managed.properties.get('last_username', ''), 'local_user_name')
# TODO: Finish this test
@ -102,7 +119,6 @@ class ServerEventsLoginLogoutTest(rest.test.RESTTestCase):
# 'user_service': 'uuid', # MUST BE PRESENT
# 'username': 'username', # Optional
# }
response = self.client.rest_post(
'/servers/event',
data={
@ -110,6 +126,7 @@ class ServerEventsLoginLogoutTest(rest.test.RESTTestCase):
'type': 'logout',
'user_service': self.user_service_managed.uuid,
'username': 'local_user_name',
'session_id': '',
},
)
self.assertEqual(response.status_code, 200)
@ -129,3 +146,48 @@ class ServerEventsLoginLogoutTest(rest.test.RESTTestCase):
self.assertIsNotNone(response.content)
self.assertIsNotNone(response.json())
self.assertIn('error', response.json())
# No session id, shouls return error
response = self.client.rest_post(
'/servers/event',
data={
'token': self.server.token,
'type': 'logout',
'user_service': self.user_service_managed.uuid,
'username': 'local_user_name',
},
)
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.content)
self.assertIsNotNone(response.json())
self.assertIn('error', response.json())
def test_loging_logout(self) -> None:
response = self.client.rest_post(
'/servers/event',
data={
'token': self.server.token,
'type': 'login',
'user_service': self.user_service_managed.uuid,
'username': 'local_user_name',
},
)
self.assertEqual(response.status_code, 200)
session_id = response.json()['result']['session_id']
self.assertIsNotNone(session_id)
self.user_service_managed.refresh_from_db()
self.assertEqual(self.user_service_managed.in_use, True)
response = self.client.rest_post(
'/servers/event',
data={
'token': self.server.token,
'type': 'logout',
'user_service': self.user_service_managed.uuid,
'username': 'local_user_name',
'session_id': session_id,
},
)
self.assertEqual(response.status_code, 200)
self.user_service_managed.refresh_from_db()
self.assertEqual(self.user_service_managed.in_use, False)

View File

@ -33,8 +33,9 @@ import random
import typing
from unittest import mock
from uds.core import types
from uds.core import types, consts
from uds.core.util import log
from uds.core.util.model import getSqlStamp
from ...fixtures import servers as servers_fixtures
from ...utils import random_ip_v4, random_ip_v6, random_mac, rest
@ -107,8 +108,17 @@ class ServerEventsPingTest(rest.test.RESTTestCase):
server_stats = self.server.properties.get('stats', None)
self.assertIsNotNone(server_stats)
statsResponse = types.servers.ServerStatsType.fromDict(server_stats)
# Get stats, but clear stamp
statsResponse = types.servers.ServerStatsType.fromDict(server_stats, stamp=0)
self.assertEqual(statsResponse, stats)
# Ensure that stamp is not 0 on server_stats dict
self.assertNotEqual(server_stats['stamp'], 0)
# Ensure stat is valid right now
statsResponse = types.servers.ServerStatsType.fromDict(server_stats)
self.assertTrue(statsResponse.is_valid)
statsResponse = types.servers.ServerStatsType.fromDict(server_stats, stamp=getSqlStamp() - consts.DEFAULT_CACHE_TIMEOUT - 1)
self.assertFalse(statsResponse.is_valid)
def test_event_ping_without_stats(self) -> None:
# Create an stat object

View File

@ -203,7 +203,7 @@ class Client(Handler):
# ensures that we mark the service as accessed by client
# so web interface can show can react to this
if userService:
userService.properties['accessedByClient'] = True
userService.properties['accessed_by_client'] = True
def get(self) -> typing.Dict[str, typing.Any]:
"""

View File

@ -163,7 +163,7 @@ class ServersServers(DetailHandler):
return {'field': 'maintenance_mode', 'prefix': 'row-maintenance-'}
def getGui(self, parent: 'models.ServerGroup', forType: str = '') -> typing.List[typing.Any]:
kind, subkind = parent.serverType, parent.subtype
kind, subkind = parent.server_type, parent.subtype
title = _('of type') + f' {subkind.upper()} {kind.name.capitalize()}'
if kind == types.servers.ServerType.UNMANAGED:
return self.addField(
@ -301,7 +301,7 @@ class ServersServers(DetailHandler):
def deleteItem(self, parent: 'models.ServerGroup', item: str) -> None:
try:
server = models.Server.objects.get(uuid=processUuid(item))
if parent.serverType == types.servers.ServerType.UNMANAGED:
if parent.server_type == types.servers.ServerType.UNMANAGED:
parent.servers.remove(server) # Remove reference
server.delete() # and delete server
else:

View File

@ -91,3 +91,8 @@ FALSE_STR: typing.Final[str] = 'false'
# Default length for Gui Text Fields
DEFAULT_TEXT_LENGTH: typing.Final[int] = 64
# Default timeouts, in seconds
DEFAULT_CACHE_TIMEOUT: typing.Final[int] = 60 * 3 # 3 minutes
LONG_CACHE_TIMEOUT: typing.Final[int] = DEFAULT_CACHE_TIMEOUT * 20 # 1 hour
SMALL_CACHE_TIMEOUT: typing.Final[int] = DEFAULT_CACHE_TIMEOUT // 3 # 1 minute

View File

@ -31,12 +31,11 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
import logging
import typing
from uds.core.util.model import getSqlDatetime
from uds.core import types, consts
from uds.REST.utils import rest_result
from uds import models
from uds.core import consts, osmanagers, types
from uds.core.util import log
from uds.core.util.model import getSqlDatetime, getSqlStamp
from uds.REST.utils import rest_result
logger = logging.getLogger(__name__)
@ -55,15 +54,49 @@ def process_log(data: typing.Dict[str, typing.Any]) -> typing.Any:
def process_login(data: typing.Dict[str, typing.Any]) -> typing.Any:
server = models.Server.objects.get(token=data['token'])
userService = models.UserService.objects.get(uuid=data['user_service'])
userService.setInUse(True)
server.setActorVersion(userService)
return rest_result(consts.OK)
if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.)
osmanagers.OSManager.loggedIn(userService, data['username'])
# Get the source of the connection and a new session id
src = userService.getConnectionSource()
session_id = userService.initSession() # creates a session for every login requested
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
maxIdle = osManager.maxIdle() if osManager else None
logger.debug('Max idle: %s', maxIdle)
deadLine = deadLine = (
userService.deployed_service.getDeadline() if not osManager or osManager.ignoreDeadLine() else None
)
return rest_result(
{
'ip': src.ip,
'hostname': src.hostname,
'dead_line': deadLine,
'max_idle': maxIdle,
'session_id': session_id,
}
)
def process_logout(data: typing.Dict[str, typing.Any]) -> typing.Any:
userService = models.UserService.objects.get(uuid=data['user_service'])
userService.setInUse(False)
session_id = data['session_id']
userService.closeSession(session_id)
if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.)
osmanagers.OSManager.loggedOut(userService, data['username'])
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if not osManager or osManager.isRemovableOnLogout(userService):
logger.debug('Removable on logout: %s', osManager)
userService.remove()
return rest_result(consts.OK)
@ -71,11 +104,9 @@ def process_logout(data: typing.Dict[str, typing.Any]) -> typing.Any:
def process_ping(data: typing.Dict[str, typing.Any]) -> typing.Any:
server = models.Server.objects.get(token=data['token'])
if 'stats' in data:
# Load anc check stats
stats = types.servers.ServerStatsType.fromDict(data['stats'])
server.stats = types.servers.ServerStatsType.fromDict(data['stats'])
# Set stats on server
server.properties['stats'] = stats.asDict()
server.properties['last_ping'] = getSqlDatetime()
server.last_ping = getSqlDatetime()
return rest_result(consts.OK)
@ -103,7 +134,7 @@ def process(data: typing.Dict[str, typing.Any]) -> typing.Any:
return rest_result('error', error=f'Invalid event type {data.get("type", "not_found")}')
try:
fnc(data)
return fnc(data)
except Exception as e:
logger.error('Exception processing event %s: %s', data, e)
return rest_result('error', error=str(e))

View File

@ -221,7 +221,7 @@ class OSManager(Module):
'''
Resets login counter to 0
'''
userService.properties['loginsCounter'] = 0
userService.properties['logins_counter'] = 0
# And execute ready notification method
self.readyNotified(userService)
@ -235,6 +235,7 @@ class OSManager(Module):
"""
uniqueId = userService.unique_id
userService.setInUse(True)
userService.properties['last_username'] = userName or 'unknown' # Store it for convenience
userServiceInstance = userService.getInstance()
userServiceInstance.userLoggedIn(userName or 'unknown')
userService.updateData(userServiceInstance)
@ -277,8 +278,8 @@ class OSManager(Module):
# Context makes a transaction, so we can use it to update the counter
with userService.properties as p:
counter = int(typing.cast(str, p.get('loginsCounter', 0))) + 1
p['loginsCounter'] = counter
counter = int(typing.cast(str, p.get('logins_counter', 0))) + 1
p['logins_counter'] = counter
@staticmethod
def loggedOut(userService: 'UserService', userName: typing.Optional[str] = None) -> None:
@ -289,10 +290,10 @@ class OSManager(Module):
- Invokes userLoggedIn for user service instance
"""
with userService.properties as p:
counter = int(typing.cast(str, p.get('loginsCounter', 0))) - 1
counter = int(typing.cast(str, p.get('logins_counter', 0))) - 1
if counter > 0:
counter -= 1
p['loginsCounter'] = counter
p['logins_counter'] = counter
if GlobalConfig.EXCLUSIVE_LOGOUT.getBool(True) and counter > 0:
return

View File

@ -30,12 +30,12 @@
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import enum
import time
import datetime
import typing
from django.utils.translation import gettext as _
from uds.core.consts import images
from uds.core import consts
from uds.core.util import singleton
from uds.core.util.model import getSqlStamp
@ -110,7 +110,7 @@ class ServerSubType(metaclass=singleton.Singleton):
# I.e. "linuxapp" will be registered by the Linux Applications Provider
# The main usage of this subtypes is to allow to group servers by type, and to allow to filter by type
ServerSubType.manager().register(
ServerType.UNMANAGED, 'ip', 'Unmanaged IP Server', images.DEFAULT_IMAGE_BASE64, False
ServerType.UNMANAGED, 'ip', 'Unmanaged IP Server', consts.images.DEFAULT_IMAGE_BASE64, False
)
@ -132,6 +132,16 @@ class ServerStatsType(typing.NamedTuple):
def memfree_ratio(self) -> float:
return (self.memtotal - self.memused) / (self.memtotal or 1) / (self.current_users + 1)
@property
def is_valid( self ) -> bool:
"""If the stamp is lesss than consts.DEFAULT_CACHE_TIMEOUT, it is considered valid
Returns:
bool: True if valid, False otherwise
"""
return self.stamp > getSqlStamp() - consts.DEFAULT_CACHE_TIMEOUT
def weight(self, minMemory: int = 0) -> float:
# Weights are calculated as:
# 0.5 * cpu_usage + 0.5 * (1 - mem_free / mem_total) / (current_users + 1)
@ -145,7 +155,9 @@ class ServerStatsType(typing.NamedTuple):
return 1 / ((self.cpufree_ratio * 1.3 + self.memfree_ratio) or 1)
@staticmethod
def fromDict(dct: typing.Dict[str, typing.Any]) -> 'ServerStatsType':
def fromDict(data: typing.Mapping[str, typing.Any], **kwargs: typing.Any) -> 'ServerStatsType':
dct = { k:v for k, v in data.items()} # Make a copy
dct.update(kwargs) # and update with kwargs
disks: typing.List[typing.Tuple[str, int, int]] = []
for disk in dct.get('disks', []):
disks.append((disk['name'], disk['used'], disk['total']))

View File

@ -656,7 +656,7 @@ class gui:
def _setValue(self, value: typing.Any):
# Internally stores an string
super()._setValue(str(value))
super()._setValue(value)
def num(self) -> int:
"""

View File

@ -28,25 +28,26 @@
'''
Author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import typing
import datetime
import secrets
import typing
from django.db import models
from uds.core import types
from uds.core.util.os_detector import KnownOS
from uds.core import consts, types
from uds.core.consts import MAC_UNKNOWN, MAX_DNS_NAME_LENGTH, MAX_IPV6_LENGTH, SERVER_DEFAULT_LISTEN_PORT
from uds.core.types.request import ExtendedHttpRequest
from uds.core.util import properties, log, resolver, net
from uds.core.util import log, net, properties, resolver
from uds.core.util.model import getSqlStamp
from uds.core.util.os_detector import KnownOS
from uds.core.consts import MAX_DNS_NAME_LENGTH, MAX_IPV6_LENGTH, MAC_UNKNOWN, SERVER_DEFAULT_LISTEN_PORT
from .uuid_model import UUIDModel
from .tag import TaggingMixin
from .uuid_model import UUIDModel
if typing.TYPE_CHECKING:
from uds.models.transport import Transport
from uds.models.user import User
from uds.models.user_service import UserService
class ServerGroup(UUIDModel, TaggingMixin, properties.PropertiesMixin):
@ -95,11 +96,17 @@ class ServerGroup(UUIDModel, TaggingMixin, properties.PropertiesMixin):
return f'{self.host}:{self.port}'
@property
def serverType(self) -> types.servers.ServerType:
def server_type(self) -> types.servers.ServerType:
"""Returns the server type of this server"""
try:
return types.servers.ServerType(self.type)
except ValueError:
return types.servers.ServerType.UNMANAGED # Invalid value, return default
return types.servers.ServerType.UNMANAGED
@server_type.setter
def server_type(self, value: types.servers.ServerType) -> None:
"""Sets the server type of this server"""
self.type = value
def https_url(self, path: str) -> str:
if not path.startswith('/'):
@ -200,42 +207,12 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
return self.uuid, 'server'
@property
def serverType(self) -> types.servers.ServerType:
def server_type(self) -> types.servers.ServerType:
"""Returns the server type of this server"""
try:
return types.servers.ServerType(self.type)
except ValueError:
return types.servers.ServerType.UNMANAGED # Invalid value, return default
@staticmethod
def create_token() -> str:
return create_token() # Return global function
@staticmethod
def validateToken(
token: str,
serverType: typing.Union[typing.Iterable[types.servers.ServerType], types.servers.ServerType],
request: typing.Optional[ExtendedHttpRequest] = None,
) -> bool:
# Ensure token is valid
try:
if isinstance(serverType, types.servers.ServerType):
tt = Server.objects.get(token=token, type=serverType.value)
else:
tt = Server.objects.get(token=token, type__in=[st.value for st in serverType])
# We could check the request ip here
if request and request.ip != tt.ip:
raise Exception('Invalid ip')
return True
except Server.DoesNotExist:
pass
except Server.MultipleObjectsReturned:
raise Exception('Multiple objects returned for token')
return False
@property
def server_type(self) -> types.servers.ServerType:
"""Returns the server type of this server"""
return types.servers.ServerType(self.type)
return types.servers.ServerType.UNMANAGED
@server_type.setter
def server_type(self, value: types.servers.ServerType) -> None:
@ -262,6 +239,76 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
"""Returns the ip version of this server"""
return 6 if ':' in self.ip else 4
@property
def stats(self) -> typing.Optional[types.servers.ServerStatsType]:
"""Returns the current stats of this server, or None if not available"""
statsDct = self.properties.get('stats', None)
if statsDct:
stats = types.servers.ServerStatsType.fromDict(statsDct)
if stats.is_valid:
return stats
return None
@stats.setter
def stats(self, value: typing.Optional[types.servers.ServerStatsType]) -> None:
"""Sets the current stats of this server"""
if value is None:
del self.properties['stats']
else:
# Set stamp to current time and save it
statsDict = value.asDict()
statsDict['stamp'] = getSqlStamp()
self.properties['stats'] = statsDict
@property
def last_ping(self) -> datetime.datetime:
"""Returns the last ping of this server"""
return self.properties.get('last_ping', consts.NEVER)
@last_ping.setter
def last_ping(self, value: datetime.datetime) -> None:
"""Sets the last ping of this server"""
self.properties['last_ping'] = value
@staticmethod
def create_token() -> str:
return create_token() # Return global function
@staticmethod
def validateToken(
token: str,
serverType: typing.Union[typing.Iterable[types.servers.ServerType], types.servers.ServerType],
request: typing.Optional[ExtendedHttpRequest] = None,
) -> bool:
"""Ensures that a token is valid for a server type
Args:
token: Token to validate
serverType: Server type to validate token for
request: Optional request to check ip against token ip
Returns:
True if token is valid for server type, False otherwise
Note:
This allows to keep Tunnels, Servers, Actors.. etc on same table, and validate tokens for each kind
"""
# Ensure token is valid for a kind
try:
if isinstance(serverType, types.servers.ServerType):
tt = Server.objects.get(token=token, type=serverType.value)
else:
tt = Server.objects.get(token=token, type__in=[st.value for st in serverType])
# We could check the request ip here
if request and request.ip != tt.ip:
raise Exception('Invalid ip')
return True
except Server.DoesNotExist:
pass
except Server.MultipleObjectsReturned:
raise Exception('Multiple objects returned for token')
return False
@staticmethod
def search(ip_or_host: str) -> typing.Optional['Server']:
"""Locates a server by ip or hostname
@ -283,6 +330,10 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
found = Server.objects.filter(hostname=ip_or_host).first()
return found
def setActorVersion(self, userService: 'UserService') -> None:
"""Sets the actor version of this server to the userService"""
userService.setActorVersion(f'Server {self.version or "unknown"}')
def getCommsUrl(self, *, path: typing.Optional[str] = None) -> typing.Optional[str]:
"""
Returns the url for a path to this server

View File

@ -419,7 +419,7 @@ def enableService(
userService, trans = res[1], res[3]
userService.properties['accessedByClient'] = False # Reset accesed property to
userService.properties['accessed_by_client'] = False # Reset accesed property to
typeTrans = trans.getType()

View File

@ -180,7 +180,7 @@ def userServiceStatus(
ip = False
ready = 'ready'
if userService.properties.get('accessedByClient', False) is True:
if userService.properties.get('accessed_by_client', False) is True:
ready = 'accessed'
status = 'running' if ip is None else 'error' if ip is False else ready