Updated default wait-time && fixed mac access

This commit is contained in:
Adolfo Gómez García 2020-11-16 13:09:33 +01:00
parent e4345dfefa
commit 024bb5e748
7 changed files with 302 additions and 83 deletions

View File

@ -42,10 +42,11 @@ from uds.core import transports
def createADUser():
try:
from . import AD
except ImportError:
return
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds import models
@ -60,17 +61,57 @@ class BaseRDPTransport(transports.Transport):
Provides access via RDP to service.
This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password
"""
iconFile = 'rdp.png'
protocol = transports.protocols.RDP
useEmptyCreds = gui.CheckBoxField(label=_('Empty creds'), order=11, tooltip=_('If checked, the credentials used to connect will be emtpy'), tab=gui.CREDENTIALS_TAB)
fixedName = gui.TextField(label=_('Username'), order=12, tooltip=_('If not empty, this username will be always used as credential'), tab=gui.CREDENTIALS_TAB)
fixedPassword = gui.PasswordField(label=_('Password'), order=13, tooltip=_('If not empty, this password will be always used as credential'), tab=gui.CREDENTIALS_TAB)
withoutDomain = gui.CheckBoxField(label=_('Without Domain'), order=14, tooltip=_('If checked, the domain part will always be emptied (to connect to xrdp for example is needed)'), tab=gui.CREDENTIALS_TAB)
fixedDomain = gui.TextField(label=_('Domain'), order=15, tooltip=_('If not empty, this domain will be always used as credential (used as DOMAIN\\user)'), tab=gui.CREDENTIALS_TAB)
useEmptyCreds = gui.CheckBoxField(
label=_('Empty creds'),
order=11,
tooltip=_('If checked, the credentials used to connect will be emtpy'),
tab=gui.CREDENTIALS_TAB,
)
fixedName = gui.TextField(
label=_('Username'),
order=12,
tooltip=_('If not empty, this username will be always used as credential'),
tab=gui.CREDENTIALS_TAB,
)
fixedPassword = gui.PasswordField(
label=_('Password'),
order=13,
tooltip=_('If not empty, this password will be always used as credential'),
tab=gui.CREDENTIALS_TAB,
)
withoutDomain = gui.CheckBoxField(
label=_('Without Domain'),
order=14,
tooltip=_(
'If checked, the domain part will always be emptied (to connect to xrdp for example is needed)'
),
tab=gui.CREDENTIALS_TAB,
)
fixedDomain = gui.TextField(
label=_('Domain'),
order=15,
tooltip=_(
'If not empty, this domain will be always used as credential (used as DOMAIN\\user)'
),
tab=gui.CREDENTIALS_TAB,
)
allowSmartcards = gui.CheckBoxField(label=_('Allow Smartcards'), order=20, tooltip=_('If checked, this transport will allow the use of smartcards'), tab=gui.PARAMETERS_TAB)
allowPrinters = gui.CheckBoxField(label=_('Allow Printers'), order=21, tooltip=_('If checked, this transport will allow the use of user printers'), tab=gui.PARAMETERS_TAB)
allowSmartcards = gui.CheckBoxField(
label=_('Allow Smartcards'),
order=20,
tooltip=_('If checked, this transport will allow the use of smartcards'),
tab=gui.PARAMETERS_TAB,
)
allowPrinters = gui.CheckBoxField(
label=_('Allow Printers'),
order=21,
tooltip=_('If checked, this transport will allow the use of user printers'),
tab=gui.PARAMETERS_TAB,
)
allowDrives = gui.ChoiceField(
label=_('Local drives policy'),
order=22,
@ -81,20 +122,51 @@ class BaseRDPTransport(transports.Transport):
{'id': 'dynamic', 'text': 'Allow PnP drives'},
{'id': 'true', 'text': 'Allow any drive'},
],
tab=gui.PARAMETERS_TAB
tab=gui.PARAMETERS_TAB,
)
enforceDrives = gui.TextField(
label=_('Force drives'),
order=23,
tooltip=_('Use comma separated values, for example "C:,D:". If drives policy is disallowed, this will be ignored'),
tab=gui.PARAMETERS_TAB
tooltip=_(
'Use comma separated values, for example "C:,D:". If drives policy is disallowed, this will be ignored'
),
tab=gui.PARAMETERS_TAB,
)
allowSerials = gui.CheckBoxField(label=_('Allow Serials'), order=24, tooltip=_('If checked, this transport will allow the use of user serial ports'), tab=gui.PARAMETERS_TAB)
allowClipboard = gui.CheckBoxField(label=_('Enable clipboard'), order=25, tooltip=_('If checked, copy-paste functions will be allowed'), tab=gui.PARAMETERS_TAB, defvalue=gui.TRUE)
allowAudio = gui.CheckBoxField(label=_('Enable sound'), order=26, tooltip=_('If checked, sound will be redirected.'), tab=gui.PARAMETERS_TAB, defvalue=gui.TRUE)
allowWebcam = gui.CheckBoxField(label=_('Enable webcam'), order=27, tooltip=_('If checked, webcam will be redirected (ONLY Windows).'), tab=gui.PARAMETERS_TAB, defvalue=gui.FALSE)
credssp = gui.CheckBoxField(label=_('Credssp Support'), order=28, tooltip=_('If checked, will enable Credentials Provider Support)'), tab=gui.PARAMETERS_TAB, defvalue=gui.TRUE)
allowSerials = gui.CheckBoxField(
label=_('Allow Serials'),
order=24,
tooltip=_('If checked, this transport will allow the use of user serial ports'),
tab=gui.PARAMETERS_TAB,
)
allowClipboard = gui.CheckBoxField(
label=_('Enable clipboard'),
order=25,
tooltip=_('If checked, copy-paste functions will be allowed'),
tab=gui.PARAMETERS_TAB,
defvalue=gui.TRUE,
)
allowAudio = gui.CheckBoxField(
label=_('Enable sound'),
order=26,
tooltip=_('If checked, sound will be redirected.'),
tab=gui.PARAMETERS_TAB,
defvalue=gui.TRUE,
)
allowWebcam = gui.CheckBoxField(
label=_('Enable webcam'),
order=27,
tooltip=_('If checked, webcam will be redirected (ONLY Windows).'),
tab=gui.PARAMETERS_TAB,
defvalue=gui.FALSE,
)
credssp = gui.CheckBoxField(
label=_('Credssp Support'),
order=28,
tooltip=_('If checked, will enable Credentials Provider Support)'),
tab=gui.PARAMETERS_TAB,
defvalue=gui.TRUE,
)
screenSize = gui.ChoiceField(
label=_('Screen Size'),
@ -109,7 +181,7 @@ class BaseRDPTransport(transports.Transport):
{'id': '1920x1080', 'text': '1920x1080'},
{'id': '-1x-1', 'text': 'Full screen'},
],
tab=gui.DISPLAY_TAB
tab=gui.DISPLAY_TAB,
)
colorDepth = gui.ChoiceField(
@ -123,23 +195,105 @@ class BaseRDPTransport(transports.Transport):
{'id': '24', 'text': '24'},
{'id': '32', 'text': '32'},
],
tab=gui.DISPLAY_TAB
tab=gui.DISPLAY_TAB,
)
wallpaper = gui.CheckBoxField(label=_('Wallpaper/theme'), order=32, tooltip=_('If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)'), tab=gui.DISPLAY_TAB)
multimon = gui.CheckBoxField(label=_('Multiple monitors'), order=33, tooltip=_('If checked, all client monitors will be used for displaying (only works on windows clients)'), tab=gui.DISPLAY_TAB)
aero = gui.CheckBoxField(label=_('Allow Desk.Comp.'), order=34, tooltip=_('If checked, desktop composition will be allowed'), tab=gui.DISPLAY_TAB)
smooth = gui.CheckBoxField(label=_('Font Smoothing'), order=35, tooltip=_('If checked, fonts smoothing will be allowed'), tab=gui.DISPLAY_TAB)
showConnectionBar = gui.CheckBoxField(label=_('Connection Bar'), order=36, tooltip=_('If checked, connection bar will be shown (only on Windows clients)'), tab=gui.DISPLAY_TAB, defvalue=gui.TRUE)
wallpaper = gui.CheckBoxField(
label=_('Wallpaper/theme'),
order=32,
tooltip=_(
'If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)'
),
tab=gui.DISPLAY_TAB,
)
multimon = gui.CheckBoxField(
label=_('Multiple monitors'),
order=33,
tooltip=_(
'If checked, all client monitors will be used for displaying (only works on windows clients)'
),
tab=gui.DISPLAY_TAB,
)
aero = gui.CheckBoxField(
label=_('Allow Desk.Comp.'),
order=34,
tooltip=_('If checked, desktop composition will be allowed'),
tab=gui.DISPLAY_TAB,
)
smooth = gui.CheckBoxField(
label=_('Font Smoothing'),
order=35,
tooltip=_('If checked, fonts smoothing will be allowed'),
tab=gui.DISPLAY_TAB,
)
showConnectionBar = gui.CheckBoxField(
label=_('Connection Bar'),
order=36,
tooltip=_('If checked, connection bar will be shown (only on Windows clients)'),
tab=gui.DISPLAY_TAB,
defvalue=gui.TRUE,
)
multimedia = gui.CheckBoxField(label=_('Multimedia sync'), order=40, tooltip=_('If checked. Linux client will use multimedia parameter for xfreerdp'), tab='Linux Client')
alsa = gui.CheckBoxField(label=_('Use Alsa'), order=41, tooltip=_('If checked, Linux client will try to use ALSA, otherwise Pulse will be used'), tab='Linux Client')
redirectHome = gui.CheckBoxField(label=_('Redirect home folder'), order=42, tooltip=_('If checked, Linux client will try to redirect /home local folder'), tab='Linux Client', defvalue=gui.FALSE)
printerString = gui.TextField(label=_('Printer string'), order=43, tooltip=_('If printer is checked, the printer string used with xfreerdp client'), tab='Linux Client', length=256)
smartcardString = gui.TextField(label=_('Smartcard string'), order=44, tooltip=_('If smartcard is checked, the smartcard string used with xfreerdp client'), tab='Linux Client', length=256)
customParameters = gui.TextField(label=_('Custom parameters'), order=45, tooltip=_('If not empty, extra parameter to include for Linux Client (for example /usb:id,dev:054c:0268, or aything compatible with your xfreerdp client)'), tab='Linux Client', length=256)
multimedia = gui.CheckBoxField(
label=_('Multimedia sync'),
order=40,
tooltip=_(
'If checked. Linux client will use multimedia parameter for xfreerdp'
),
tab='Linux Client',
)
alsa = gui.CheckBoxField(
label=_('Use Alsa'),
order=41,
tooltip=_(
'If checked, Linux client will try to use ALSA, otherwise Pulse will be used'
),
tab='Linux Client',
)
redirectHome = gui.CheckBoxField(
label=_('Redirect home folder'),
order=42,
tooltip=_('If checked, Linux client will try to redirect /home local folder'),
tab='Linux Client',
defvalue=gui.FALSE,
)
printerString = gui.TextField(
label=_('Printer string'),
order=43,
tooltip=_(
'If printer is checked, the printer string used with xfreerdp client'
),
tab='Linux Client',
length=256,
)
smartcardString = gui.TextField(
label=_('Smartcard string'),
order=44,
tooltip=_(
'If smartcard is checked, the smartcard string used with xfreerdp client'
),
tab='Linux Client',
length=256,
)
customParameters = gui.TextField(
label=_('Custom parameters'),
order=45,
tooltip=_(
'If not empty, extra parameter to include for Linux Client (for example /usb:id,dev:054c:0268, or aything compatible with your xfreerdp client)'
),
tab='Linux Client',
length=256,
)
allowMacMSRDC = gui.CheckBoxField(label=_('Allow Microsoft Rdp Client'), order=40, tooltip=_('If checked, allows use of Microsoft Remote Desktop Clien. PASSWORD WILL BE PRONPTED!'), tab='Mac OS X', defvalue=gui.FALSE)
allowMacMSRDC = gui.CheckBoxField(
label=_('Allow Microsoft Rdp Client'),
order=40,
tooltip=_(
'If checked, allows use of Microsoft Remote Desktop Client. PASSWORD WILL BE PROMPTED!'
),
tab='Mac OS X',
defvalue=gui.FALSE,
)
def isAvailableFor(self, userService: 'models.UserService', ip: str) -> bool:
"""
@ -157,11 +311,15 @@ class BaseRDPTransport(transports.Transport):
self.cache.put(ip, 'N', READY_CACHE_TIMEOUT)
return ready == 'Y'
def processedUser(self, userService: 'models.UserService', user: 'models.User') -> str:
def processedUser(
self, userService: 'models.UserService', user: 'models.User'
) -> str:
v = self.processUserPassword(userService, user, '')
return v['username']
def processUserPassword(self, userService: 'models.UserService', user: 'models.User', password: 'str') -> typing.Dict[str, typing.Any]:
def processUserPassword(
self, userService: 'models.UserService', user: 'models.User', password: 'str'
) -> typing.Dict[str, typing.Any]:
username = user.getUsernameForAuth()
if self.fixedName.value:
@ -201,22 +359,31 @@ class BaseRDPTransport(transports.Transport):
if '\\' in username:
domain, username = username.split('\\')
return {'protocol': self.protocol, 'username': username, 'password': password, 'domain': domain}
return {
'protocol': self.protocol,
'username': username,
'password': password,
'domain': domain,
}
def getConnectionInfo(
self,
userService: typing.Union['models.UserService', 'models.ServicePool'],
user: 'models.User',
password: str
) -> typing.Dict[str, str]:
self,
userService: typing.Union['models.UserService', 'models.ServicePool'],
user: 'models.User',
password: str,
) -> typing.Dict[str, str]:
return self.processUserPassword(userService, user, password)
def getScript(self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any]) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]:
def getScript(
self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any]
) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]:
# Reads script
scriptNameTemplate = scriptNameTemplate.format(osName)
with open(os.path.join(os.path.dirname(__file__), scriptNameTemplate)) as f:
script = f.read()
# Reads signature
with open(os.path.join(os.path.dirname(__file__), scriptNameTemplate + '.signature')) as f:
with open(
os.path.join(os.path.dirname(__file__), scriptNameTemplate + '.signature')
) as f:
signature = f.read()
return script, signature, params

View File

@ -67,7 +67,7 @@ class RDPFile:
smartcardString = None
enablecredsspsupport = False
enableClipboard = False
linuxCustomParameters = None
linuxCustomParameters: typing.Optional[str] = None
enforcedShares: typing.Optional[str] = None
def __init__(
@ -183,7 +183,7 @@ class RDPFile:
if forceRDPSecurity:
params.append('/sec:rdp')
if self.linuxCustomParameters is not None and self.linuxCustomParameters.strip() != '':
if self.linuxCustomParameters and self.linuxCustomParameters.strip() != '':
params += shlex.split(self.linuxCustomParameters.strip())
return params
@ -191,15 +191,15 @@ class RDPFile:
def getGeneric(self): # pylint: disable=too-many-statements
password = '{password}'
screenMode = '2' if self.fullScreen else '1'
audioMode = self.redirectAudio and "0" or "2"
serials = self.redirectSerials and "1" or "0"
scards = self.redirectSmartcards and "1" or "0"
printers = self.redirectPrinters and "1" or "0"
compression = self.compression and "1" or "0"
connectionBar = self.displayConnectionBar and "1" or "0"
disableWallpaper = self.showWallpaper and "0" or "1"
useMultimon = self.multimon and "1" or "0"
enableClipboard = self.enableClipboard and "1" or "0"
audioMode = '2' if self.redirectAudio else '0'
serials = '1' if self.redirectSerials else '0'
scards = '1' if self.redirectSmartcards else '0'
printers = '1' if self.redirectPrinters else '0'
compression = '1' if self.compression else '0'
connectionBar = '1' if self.displayConnectionBar else '0'
disableWallpaper = '1' if self.showWallpaper else '0'
useMultimon = '1' if self.multimon else '0'
enableClipboard = '1' if self.enableClipboard else '0'
res = ''
res += 'screen mode id:i:' + screenMode + '\n'

View File

@ -57,19 +57,37 @@ READY_CACHE_TIMEOUT = 30
class TRDPTransport(BaseRDPTransport):
'''
"""
Provides access via RDP to service.
This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password
'''
"""
typeName = _('RDP')
typeType = 'TSRDPTransport'
typeDescription = _('RDP Protocol. Tunneled connection.')
group = transports.TUNNELED_GROUP
tunnelServer = gui.TextField(label=_('Tunnel server'), order=1, tooltip=_('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)'), tab=gui.TUNNEL_TAB)
tunnelServer = gui.TextField(
label=_('Tunnel server'),
order=1,
tooltip=_(
'IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)'
),
tab=gui.TUNNEL_TAB,
)
# tunnelCheckServer = gui.TextField(label=_('Tunnel host check'), order=2, tooltip=_('If not empty, this server will be used to check if service is running before assigning it to user. (use HOST:PORT format)'), tab=gui.TUNNEL_TAB)
tunnelWait = gui.NumericField(length=3, label=_('Tunnel wait time'), defvalue='10', minValue=1, maxValue=65536, order=2, tooltip=_('Maximum time to wait before closing the tunnel listener'), required=True, tab=gui.TUNNEL_TAB)
tunnelWait = gui.NumericField(
length=3,
label=_('Tunnel wait time'),
defvalue='30',
minValue=5,
maxValue=65536,
order=2,
tooltip=_('Maximum time to wait before closing the tunnel listener'),
required=True,
tab=gui.TUNNEL_TAB,
)
useEmptyCreds = BaseRDPTransport.useEmptyCreds
fixedName = BaseRDPTransport.fixedName
@ -106,18 +124,20 @@ class TRDPTransport(BaseRDPTransport):
def initialize(self, values: 'Module.ValuesType'):
if values:
if values['tunnelServer'].count(':') != 1:
raise transports.Transport.ValidationException(_('Must use HOST:PORT in Tunnel Server Field'))
raise transports.Transport.ValidationException(
_('Must use HOST:PORT in Tunnel Server Field')
)
def getUDSTransportScript( # pylint: disable=too-many-locals
self,
userService: 'models.UserService',
transport: 'models.Transport',
ip: str,
os: typing.Dict[str, str],
user: 'models.User',
password: str,
request: 'HttpRequest'
) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]:
self,
userService: 'models.UserService',
transport: 'models.Transport',
ip: str,
os: typing.Dict[str, str],
user: 'models.User',
password: str,
request: 'HttpRequest',
) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]:
# We use helper to keep this clean
# prefs = user.prefs('rdp')
@ -132,14 +152,19 @@ class TRDPTransport(BaseRDPTransport):
width, height = self.screenSize.value.split('x')
depth = self.colorDepth.value
tunpass = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _i in range(12))
tunpass = ''.join(
random.SystemRandom().choice(string.ascii_letters + string.digits)
for _i in range(12)
)
tunuser = TicketStore.create(tunpass)
sshHost, sshPort = self.tunnelServer.value.split(':')
logger.debug('Username generated: %s, password: %s', tunuser, tunpass)
r = RDPFile(width == '-1' or height == '-1', width, height, depth, target=os['OS'])
r = RDPFile(
width == '-1' or height == '-1', width, height, depth, target=os['OS']
)
r.enablecredsspsupport = ci.get('sso') == 'True' or self.credssp.isTrue()
r.address = '{address}'
r.username = username
@ -168,11 +193,13 @@ class TRDPTransport(BaseRDPTransport):
osName = {
OsDetector.Windows: 'windows',
OsDetector.Linux: 'linux',
OsDetector.Macintosh: 'macosx'
OsDetector.Macintosh: 'macosx',
}.get(os['OS'])
if osName is None:
return super().getUDSTransportScript(userService, transport, ip, os, user, password, request)
return super().getUDSTransportScript(
userService, transport, ip, os, user, password, request
)
sp = {
'tunUser': tunuser,
@ -188,19 +215,24 @@ class TRDPTransport(BaseRDPTransport):
if osName == 'windows':
if password != '':
r.password = '{password}'
sp.update({
'as_file': r.as_file,
})
sp.update(
{
'as_file': r.as_file,
}
)
elif osName == 'linux':
sp.update({
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
})
sp.update(
{
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
}
)
else: # Mac
sp.update({
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
'as_file': r.as_file if self.allowMacMSRDC.isTrue() else '',
'as_rdp_url': r.as_rdp_url if self.allowMacMSRDC.isTrue() else '',
})
sp.update(
{
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
'as_file': r.as_file if self.allowMacMSRDC.isTrue() else '',
'as_rdp_url': r.as_rdp_url if self.allowMacMSRDC.isTrue() else '',
}
)
return self.getScript('scripts/{}/tunnel.py', osName, sp)

View File

@ -49,6 +49,16 @@ if executable is None:
<ul>
<li>
<p><b>Xfreerdp</b> from homebrew</p>
<p>
<ul>
<li>Install brew (from brew.sh website)</li>
<li>Install xquartz<br/>
<b>brew cask install xquartz</b></li>
<li>Install freerdp<br/>
<b>brew install freerdp</b></li>
<li>Reboot so xquartz will be automatically started when needed</li>
</ul>
</p>
</li>
</ul>
''')

View File

@ -1 +1 @@
Hfx61+xy7yzVCe4mq8/9rekZvTpbW64z3rPRV7ythmT+zLjOdd07nCresS/mA5FwfK1B2dGqkUw2Sj7XYfPVHp+pfxa1MSVDeAeyh12mhLNi9AcEvrkRw7kfQ59ZHgm4fZvAtOpkWqN/pU+V73T5eTdhIBATRS9PCwJwbYOTiEw7ndF+nZpvW/n6E0grpSoqr2QuEqj8tK+4Sb7OTtwHeYku3KDM/CZ8RYP5+OLW1OcyjpqkbbL9xgN+8zPu0wNpBMHSzwRNkQ8I0WVvWhtyhbx2RpwaFBd2ETOAUzaczJoZ/R8dNOBqBbqUk3YkuJdGhUQ+bo8ZL9Ga14Ew/wKyht+G9IkiyLzVgOxKCPT05g0ept2DU2PqJw3isK3vzkhHV7JQC7T75Aqouc/N37YNcv/OWsTD+HcLHpZoo8kMaG6PQxyfg9smrcuJPoeOHVbklRLR7cptuZ8HPEyTCKjvCx5rpi2uvXw/LsHsEWesqYt69o8BOOMhm23Hz0LK/ZAhQ7Ll/9p17+g/JeUIFeBxuXNg5A28fvTdbK6GwzbSf54Y7/woyFFcw+ii/FRUpjehN0R9M6JVqLa0XTYgQuIwrPApL5uepdZZcNFXn/ax9lK7HPkJy55iun4buQFODxMi4slgJaPrjkiY8/LwQzh0cohwWBOy05U2nOEahxUQXrA=
NL6cwtXIFixi6D/z5uksdNnAzd7RSP6xG92DzQ84XCGhBaYHtsMdk7pPMo/zwI6mMNyKJLqvwSUaS3eCdLU7SnTM3xAj4SSGSbFTNRD6q+2f2K2xU3etiGQagQ3vcmt+o79JMXP80qsfYA717fPlRLii7WRm56w5FwgiUWEmas4WIw6e7oXe6ucQoJJ9X4P6w3K5gR4cGBOHOz5HNxrIxy2d+OcahLV2du9efzaB+qJNpqon2ETkS0y3b5jO+xWcsChRGjzpdgcqXd6EkTz/Sow4zzQoeunkESz9TShNou3lJC8ITlBN+FKgJujh7wzpVDvcDIqRwQhkD7vaMsu+Yu/47LBhY30LHesmQcc7av5kkcCLWkz3QCRQo49G2/Flw6PuIidrxj1k5H/+yA0ktwXNB4EUsPtePLpWaYo0xZuaM1fr3kDpAcSAaKSIeAxJ2Oxl9HLNnBDwyTaZzZiIl23tbRIJeSDL9C42qXqgNWvq44TNxtuDpItJNyu1ov+ZJJaPxlYiC6C7v0sNNsvOiCJXl3CLS56v65W4xZSxAfC4juybRA09RugfpMO+N/hS/xGbaNVsYvEmS/UGVKdFBgrtF/SwTYO3mBzhbAyeKQhV7Ovd5Vs5noKd4DbZG7XKNASqbKWwkrZFzNxX2mYJpr9ZnACr4aJ+RnM38KDEGOQ=

View File

@ -52,6 +52,16 @@ if executable is None:
<ul>
<li>
<p><b>Xfreerdp</b> from homebrew</p>
<p>
<ul>
<li>Install brew (from brew.sh website)</li>
<li>Install xquartz<br/>
<b>brew cask install xquartz</b></li>
<li>Install freerdp<br/>
<b>brew install freerdp</b></li>
<li>Reboot so xquartz will be automatically started when needed</li>
</ul>
</p>
</li>
</ul>
''')

View File

@ -1 +1 @@
k9goVY5L7HOfpHjmcqnIiAaDnSVwlBJ/DNOPb1PL1XwpfaAgYp2qlAyY/fbi1YLWfAsbJhdhWgJH9iF1KLEx3Jn1P5J4ObpxKkVmjUW0oYF4qZdkZrik8VtHCgvwDkEPHyP/uzIwMWb8ydiTxoUBH+ZJTbo0qeQ7wgbnfljckJHNnWurVBtp2rFxjolQFR89cOGkLkvdPxHqcHAj1dxUkJdhmka9z3bVyb5NhmX8mtCvpR5QM3DHo8lRYHQxwtnRZooIrIr+5zKflzXoZnRn189tF2yCyk349MeUX3fnm8F22DpCP1HBkN5A9ACkRfANawbDlyR1ynoiApqEqLPM3aKojxifESNdGaD8pf9MXVsxzk+ufAphzRnU6lh3As4/+NNfJ3zbTbD6DFfj+oez0zEcF1QcA40z55+u8CYChltduJCwu1qUS0I+exAlLpEn9riJd2KmgxlLDDx/9EJjXOwVoUXKOteN4kHQXtMRB3CWneHezfgA6RdRolxxy5EBK71mwDf8BcG8seyBRXcd+p+mbhBcJPqdI/eci9g5h3JFHolOINsbNt2PZk23j7nvHRfArFCvJN1dkz7XZpgDPznvE+B0SzQHKEbGvybESlOfdo+DiFWtwt8mueTP6sMqncced+ztUsHdHDxFiVrIyh3KVzZiyEl2YFJxkLdrRH0=
ZY1MWtxfNim0HgF+EKH7+DvyjKKbB+BvbNALHHcjeiiCnazZOI9ToMghEPUvMosPKWrbGJ+bmIJDdxpCLvIJJi2Vi9QHkXg9FiRR/1FJwwhLYiinv2bQa+ChYoEfVVZidCqFcDhaaIit9Kw/nBAa0luqZUYVB3ZUwbMgHPd4RNWCvcyGM2P6s+RzJyASajn2LPNrWqik2qQa2+ZHLR8mcsJ9+VYJApSbYhbOFyeM/QJCrhS7U9FQv9vJQGZES+UvcFYZy99QRv4V8Ko5izZ96EeVVaod3IXkGLd7HvC1fDeL7JZeQORJC3HZkLwslqqux9BX/1kb5XQV6qBDTsEpk3o/tw7PJVMHOu8l+LfNJnxFCyEwRkELhZeeZxvgAHYqYNuJyOsY6JyRu9okxwtAMkxYDJBWpDq9LAlHyPEZek3TXVrM+OAkz33TOM1AuDqToMadLM4tY2fsWCIEdEMCF9GFUEfGT2BT/U6x9JtFM8I8HVpdxXMkrSTagHkArmcX9+AbjifrBh6KYXFxiCPV/HZHRT0IrQ8JeaquUxKC9IRdCqVCiN6Nwq/Ww1hXzsYY07iEe/6xV5/deJhr8dLzM0+hDwnp2ag3T0DEghlBIZRZqV/CeSVPngespiyfOmbUQSpiokGtiYO6bHD8MHcgwn9uzr3xD9MCsArldpn9TSA=