forked from shaba/openuds
Advancing a bit more towards new uds transport model
This commit is contained in:
parent
38b7ac892e
commit
2303861650
@ -59,7 +59,6 @@ class Transport(Module):
|
||||
typeType = 'Base Transport'
|
||||
typeDescription = 'Base Transport'
|
||||
iconFile = 'transport.png'
|
||||
needsJava = False # If this transport needs java for rendering
|
||||
# Supported names for OS (used right now, but lots of more names for sure)
|
||||
# Windows
|
||||
# Macintosh
|
||||
@ -70,6 +69,9 @@ class Transport(Module):
|
||||
webTransport = False
|
||||
tcTransport = False
|
||||
|
||||
# If the link to use transport is provided by transport itself
|
||||
ownLink = False
|
||||
|
||||
# Protocol "type". This is not mandatory, but will help
|
||||
protocol = protocols.NONE
|
||||
|
||||
@ -158,9 +160,18 @@ class Transport(Module):
|
||||
|
||||
def getUDSTransportData(self, userService, transport, ip, os, user, password, request):
|
||||
'''
|
||||
Must ve overriden
|
||||
Must override if transport does not provides its own link (that is, it is an UDS native transport)
|
||||
Returns the transport data needed to connect with the userService
|
||||
This is invoked right before service is accesed (secuentally)
|
||||
This is invoked right before service is accesed (secuentally).
|
||||
|
||||
The class must provide either this method or the getLink method
|
||||
'''
|
||||
return None
|
||||
|
||||
def getLink(self, userService, transport, ip, os, user, password, request):
|
||||
'''
|
||||
Must override if transport does provides its own link
|
||||
If transport provides own link, this method provides the link itself
|
||||
'''
|
||||
return None
|
||||
|
||||
|
@ -41,6 +41,16 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def udsLink(request, ticket, scrambler, transport):
|
||||
|
||||
if request.is_secure():
|
||||
proto = 'udss'
|
||||
else:
|
||||
proto = 'uds'
|
||||
|
||||
return "{}://{}{}/{}/{}".format(proto, request.build_absolute_uri('/').split('//')[1], ticket, scrambler, transport.uuid)
|
||||
|
||||
|
||||
def parseDate(dateToParse):
|
||||
import datetime
|
||||
|
||||
|
@ -34,6 +34,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from uds.core.util.Cache import Cache
|
||||
from uds.core.jobs.Job import Job
|
||||
from uds.models import TicketStore
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -51,3 +52,17 @@ class CacheCleaner(Job):
|
||||
logger.debug('Starting cache cleanup')
|
||||
Cache.cleanUp()
|
||||
logger.debug('Done cache cleanup')
|
||||
|
||||
|
||||
class TicketStoreCleaner(Job):
|
||||
|
||||
frequency = 3600 * 12 # every twelve hours
|
||||
friendly_name = 'Ticket Storage Cleaner'
|
||||
|
||||
def __init__(self, environment):
|
||||
super(TicketStoreCleaner, self).__init__(environment)
|
||||
|
||||
def run(self):
|
||||
logger.debug('Starting ticket storage cleanup')
|
||||
TicketStore.cleanup()
|
||||
logger.debug('Done ticket storage cleanup')
|
@ -36,6 +36,8 @@ from django.http import HttpResponse
|
||||
from uds.core.util.Cache import Cache
|
||||
from uds.core.util import net
|
||||
from uds.core.auths import auth
|
||||
from uds.models import TicketStore
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -47,7 +49,7 @@ CONTENT_TYPE = 'text/plain'
|
||||
|
||||
|
||||
def dict2resp(dct):
|
||||
return '\r'.join((k + '\t' + v for k, v in dct.iteritems()))
|
||||
return '\r'.join((k + '\t' + v for k, v in dct.iteritems()))
|
||||
|
||||
|
||||
@auth.trustedSourceRequired
|
||||
@ -55,18 +57,14 @@ def guacamole(request, tunnelId):
|
||||
logger.debug('Received credentials request for tunnel id {0}'.format(tunnelId))
|
||||
|
||||
try:
|
||||
cache = Cache('guacamole')
|
||||
|
||||
val = cache.get(tunnelId, None)
|
||||
|
||||
val = TicketStore.get(tunnelId, invalidate=False)
|
||||
# Remove key from cache, just 1 use
|
||||
# Cache has a limit lifetime, so we will allow to "reload" the page
|
||||
# cache.remove(tunnelId)
|
||||
|
||||
# response = 'protocol\trdp\rhostname\tw7adolfo\rusername\tadmin\rpassword\ttemporal'
|
||||
response = dict2resp(val)
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
return HttpResponse(ERROR, content_type=CONTENT_TYPE)
|
||||
|
||||
return HttpResponse(response, content_type=CONTENT_TYPE)
|
||||
|
@ -53,6 +53,8 @@ class TicketStore(UUIDModel):
|
||||
This is intended for small images (i will limit them to 128x128), so storing at db is fine
|
||||
'''
|
||||
DEFAULT_VALIDITY = 60
|
||||
MAX_VALIDITY = 60 * 60 * 12
|
||||
# Cleanup will purge all elements that have been created MAX_VALIDITY ago
|
||||
|
||||
stamp = models.DateTimeField() # Date creation or validation of this entry
|
||||
validity = models.IntegerField(default=60) # Duration allowed for this ticket to be valid, in seconds
|
||||
@ -133,6 +135,13 @@ class TicketStore(UUIDModel):
|
||||
except TicketStore.DoesNotExist:
|
||||
raise Exception('Does not exists')
|
||||
|
||||
@staticmethod
|
||||
def cleanup():
|
||||
now = getSqlDatetime()
|
||||
cleanSince = now - datetime.timedelta(seconds=TicketStore.MAX_VALIDITY)
|
||||
number = TicketStore.objects.filter(stamp__lt=cleanSince).delete()
|
||||
logger.debug('Cleaned {} tickets'.format(number))
|
||||
|
||||
def __unicode__(self):
|
||||
if self.validator is not None:
|
||||
validator = pickle.loads(self.validator)
|
@ -97,4 +97,4 @@ from .DelayedTask import DelayedTask
|
||||
# Image galery related
|
||||
from .Image import Image
|
||||
|
||||
from .Ticket import TicketStore
|
||||
from .TicketStore import TicketStore
|
||||
|
@ -8,14 +8,14 @@
|
||||
{% for ser in services %}
|
||||
{% if ser.transports %}
|
||||
<div class="service-container">
|
||||
{% with trans=ser.transports|first numTransports=ser.transports|length link=host|add:ser.ticket|add:'/'|add:scrambler|add:'/' %}
|
||||
{% with trans=ser.transports|first numTransports=ser.transports|length %}
|
||||
<div class="service{% if ser.maintenance %} maintenance{% endif %}{% if ser.in_use %} inuse{% endif %}"
|
||||
{% if ser.maintenance %}
|
||||
data-content="{% trans "Under maintenance" %}"
|
||||
{% elif ser.in_use %}
|
||||
data-content="{%trans "Currently in use" %}"
|
||||
{% endif %}
|
||||
data-href="{{ link }}{{ trans.id }}">
|
||||
data-href="{{ trans.link }}">
|
||||
<div class="service-image">
|
||||
<img src="{% url "uds.web.views.serviceImage" idImage=ser.imageId %}" />
|
||||
</div>
|
||||
@ -36,7 +36,7 @@
|
||||
<div class="modal-body">
|
||||
<ul>
|
||||
{% for trans in ser.transports %}
|
||||
<li><a class="uds-service-transport" data-href-alt="{% url 'uds.web.views.client_downloads' %}" href="{{ link }}{{ trans.id }}"><img src="{% url "uds.web.views.transportIcon" idTrans=trans.id %}" alt="{{ trans.name }}" />{{ trans.name }}</a></li>
|
||||
<li><a class="uds-service-transport" data-href-alt="{% url 'uds.web.views.client_downloads' %}" href="{{ trans.link }}"><img src="{% url "uds.web.views.transportIcon" idTrans=trans.id %}" alt="{{ trans.name }}" />{{ trans.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@ -192,20 +192,43 @@
|
||||
$('.inuse').popover({container: 'body', trigger: 'hover', delay: { show: 500, hide: 100 }, placement: 'auto top'});
|
||||
|
||||
$('div.service:not(.maintenance)').on("click", function (event){
|
||||
var url, el;
|
||||
event.preventDefault();
|
||||
|
||||
uds.launch($(this));
|
||||
// check url
|
||||
el = $(this)
|
||||
url = el.attr('data-href');
|
||||
url = url != null ? url : el.attr('href');
|
||||
|
||||
if ( url.substring(0, 3) == 'uds' ) {
|
||||
uds.launch(el);
|
||||
} else {
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).on("mouseenter mouseleave", function (event) {
|
||||
$(this).toggleClass('over');
|
||||
});
|
||||
|
||||
$('.uds-service-transport').on("click", function (event){
|
||||
var url, el, modal;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var modal = $(this).parent().parent().parent().parent().parent().parent();
|
||||
modal = $(this).parent().parent().parent().parent().parent().parent();
|
||||
modal.modal('hide');
|
||||
uds.launch($(this));
|
||||
|
||||
// check url
|
||||
el = $(this)
|
||||
url = el.attr('data-href');
|
||||
url = url != null ? url : el.attr('href');
|
||||
|
||||
if ( url.substring(0, 3) == 'uds' ) {
|
||||
uds.launch(el);
|
||||
} else {
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
|
@ -33,16 +33,17 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_noop as _, ugettext
|
||||
from django.utils.translation import ugettext_noop as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from uds.core.ui.UserInterface import gui
|
||||
from uds.core.util.Cache import Cache
|
||||
from uds.core.util import net
|
||||
from uds.core.transports.BaseTransport import Transport
|
||||
from uds.core.transports import protocols
|
||||
from uds.core.util import connection
|
||||
from uds.core.util import OsDetector
|
||||
from uds.models import TicketStore
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -59,7 +60,8 @@ class HTML5RDPTransport(Transport):
|
||||
typeType = 'HTML5RDPTransport'
|
||||
typeDescription = _('RDP Transport using HTML5 client')
|
||||
iconFile = 'rdp.png'
|
||||
needsJava = False # If this transport needs java for rendering
|
||||
|
||||
ownLink = True
|
||||
supportedOss = OsDetector.allOss
|
||||
protocol = protocols.RDP
|
||||
|
||||
@ -131,7 +133,7 @@ class HTML5RDPTransport(Transport):
|
||||
|
||||
return {'protocol': self.protocol, 'username': username, 'password': password, 'domain': domain}
|
||||
|
||||
def renderForHtml(self, userService, transport, ip, os, user, password):
|
||||
def getLink(self, userService, transport, ip, os, user, password, request):
|
||||
ci = self.processUserPassword(userService, user, password)
|
||||
username, password, domain = ci['username'], ci['password'], ci['domain']
|
||||
|
||||
@ -155,18 +157,6 @@ class HTML5RDPTransport(Transport):
|
||||
|
||||
logger.debug('RDP Params: {0}'.format(params))
|
||||
|
||||
cache = Cache('guacamole')
|
||||
key = uuid.uuid4().hex
|
||||
cache.put(key, params)
|
||||
|
||||
url = "{0}/transport/?{1}".format(self.guacamoleServer.value, key)
|
||||
return '''
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {{
|
||||
var url = "{0}&" + window.location.protocol + "//" + window.location.host + "/";
|
||||
window.location = url;
|
||||
}})
|
||||
</script>
|
||||
<div>{1}...</div>
|
||||
'''.format(url, ugettext('Launching HTML5 RDP connection'))
|
||||
ticket = TicketStore.create(params)
|
||||
|
||||
return HttpResponseRedirect("{}/transport/?{}&{}".format(self.guacamoleServer.value, ticket, reverse('Index')))
|
||||
|
@ -35,14 +35,13 @@ js_info_dict = {
|
||||
'packages': ('uds',),
|
||||
}
|
||||
|
||||
|
||||
from django.conf.urls import patterns, include, url
|
||||
from uds.core.util.modfinder import loadModulesUrls
|
||||
from uds import REST
|
||||
|
||||
urlpatterns = patterns(
|
||||
'uds',
|
||||
(r'^$', 'web.views.index'),
|
||||
url(r'^$', 'web.views.index', name='Index'),
|
||||
(r'^login/$', 'web.views.login'),
|
||||
(r'^login/(?P<smallName>.+)$', 'web.views.login'),
|
||||
(r'^logout$', 'web.views.logout'),
|
||||
@ -54,7 +53,9 @@ urlpatterns = patterns(
|
||||
# Error URL
|
||||
(r'^error/(?P<idError>.+)$', 'web.views.error'),
|
||||
# Transport component url
|
||||
(r'^transcomp/(?P<idTransport>.+)/(?P<componentId>.+)$', 'web.views.transcomp'),
|
||||
url(r'^transcomp/(?P<idTransport>.+)/(?P<componentId>.+)$', 'web.views.transcomp', name='TransportComponent'),
|
||||
# Transport own link processor
|
||||
url(r'^trans/(?P<idService>.+)/(?P<idTransport>.+)$', 'web.views.trans', name='TransportOwnLink'),
|
||||
# Service notification url
|
||||
(r'^sernotify/(?P<idUserService>.+)/(?P<notification>.+)$', 'web.views.sernotify'),
|
||||
# Authenticators custom html
|
||||
@ -77,7 +78,6 @@ urlpatterns = patterns(
|
||||
# Ticket authentication
|
||||
url(r'^tkauth/(?P<ticketId>.+)$', 'web.views.ticketAuth', name='TicketAuth'),
|
||||
|
||||
|
||||
# XMLRPC Processor
|
||||
(r'^xmlrpc$', 'xmlrpc.views.xmlrpc'),
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
||||
'''
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__updated__ = '2015-03-18'
|
||||
__updated__ = '2015-03-23'
|
||||
|
||||
import logging
|
||||
|
||||
@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
|
||||
from .login import login, logout, customAuth
|
||||
from .index import index, about
|
||||
from .prefs import prefs
|
||||
from .service import service, transcomp, sernotify, transportIcon, serviceImage
|
||||
from .service import service, trans, transcomp, sernotify, transportIcon, serviceImage
|
||||
from .auth import authCallback, authInfo, authJava, ticketAuth
|
||||
from .download import download
|
||||
from .client_download import client_downloads
|
||||
|
@ -35,6 +35,7 @@ __updated__ = '2015-03-23'
|
||||
from django.shortcuts import render_to_response
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import redirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template import RequestContext
|
||||
|
||||
from uds.core.auths.auth import webLoginRequired, webPassword
|
||||
@ -42,6 +43,7 @@ from uds.core.auths.auth import webLoginRequired, webPassword
|
||||
from uds.models import DeployedService, Transport, UserService, Network, TicketStore
|
||||
from uds.core.util.Config import GlobalConfig
|
||||
from uds.core.util import OsDetector
|
||||
from uds.core.util import html
|
||||
|
||||
from uds.core.ui import theme
|
||||
from uds.core.managers.UserServiceManager import UserServiceManager
|
||||
@ -101,17 +103,6 @@ def index(request):
|
||||
services = []
|
||||
# Select assigned user services
|
||||
for svr in availUserServices:
|
||||
# Skip maintenance services...
|
||||
trans = []
|
||||
for t in svr.transports.all().order_by('priority'):
|
||||
typeTrans = t.getType()
|
||||
if t.validForIp(request.ip) and typeTrans.supportsOs(os['OS']):
|
||||
trans.append({'id': t.uuid, 'name': t.name, 'needsJava': t.getType().needsJava})
|
||||
if svr.deployed_service.image is not None:
|
||||
imageId = svr.deployed_service.image.uuid
|
||||
else:
|
||||
imageId = 'x' # Invalid
|
||||
|
||||
# Generate ticket
|
||||
data = {
|
||||
'type': 'A',
|
||||
@ -121,7 +112,26 @@ def index(request):
|
||||
}
|
||||
|
||||
ticket = TicketStore.create(data)
|
||||
link = '{}://{}/{}'.format(proto, ticket, scrambler)
|
||||
|
||||
trans = []
|
||||
for t in svr.transports.all().order_by('priority'):
|
||||
typeTrans = t.getType()
|
||||
if t.validForIp(request.ip) and typeTrans.supportsOs(os['OS']):
|
||||
if typeTrans.ownLink is True:
|
||||
link = reverse('TransportOwnLink', args=('A' + svr.uuid, trans.uuid))
|
||||
else:
|
||||
link = html.udsLink(request, ticket, scrambler, t)
|
||||
trans.append(
|
||||
{
|
||||
'id': t.uuid,
|
||||
'name': t.name,
|
||||
'link': link
|
||||
}
|
||||
)
|
||||
if svr.deployed_service.image is not None:
|
||||
imageId = svr.deployed_service.image.uuid
|
||||
else:
|
||||
imageId = 'x' # Invalid
|
||||
|
||||
services.append({
|
||||
'id': 'A' + svr.uuid,
|
||||
@ -131,19 +141,37 @@ def index(request):
|
||||
'show_transports': svr.deployed_service.show_transports,
|
||||
'maintenance': svr.deployed_service.service.provider.maintenance_mode,
|
||||
'in_use': svr.in_use,
|
||||
'ticket': ticket,
|
||||
})
|
||||
|
||||
logger.debug(services)
|
||||
|
||||
# Now generic user service
|
||||
for svr in availServices:
|
||||
# Generate ticket
|
||||
data = {
|
||||
'type': 'F',
|
||||
'service': svr.uuid,
|
||||
'user': request.user.uuid,
|
||||
'password': password
|
||||
}
|
||||
|
||||
ticket = TicketStore.create(data)
|
||||
|
||||
trans = []
|
||||
for t in svr.transports.all().order_by('priority'):
|
||||
if t.validForIp(request.ip):
|
||||
typeTrans = t.getType()
|
||||
if typeTrans.supportsOs(os['OS']):
|
||||
trans.append({'id': t.uuid, 'name': t.name, 'needsJava': typeTrans.needsJava})
|
||||
typeTrans = t.getType()
|
||||
if t.validForIp(request.ip) and typeTrans.supportsOs(os['OS']):
|
||||
if typeTrans.ownLink is True:
|
||||
link = reverse('TransportOwnLink', args=('F' + svr.uuid, t.uuid))
|
||||
else:
|
||||
link = html.udsLink(request, ticket, scrambler, t)
|
||||
trans.append(
|
||||
{
|
||||
'id': t.uuid,
|
||||
'name': t.name,
|
||||
'link': link
|
||||
}
|
||||
)
|
||||
if svr.image is not None:
|
||||
imageId = svr.image.uuid
|
||||
else:
|
||||
@ -156,16 +184,6 @@ def index(request):
|
||||
else:
|
||||
in_use = ads.in_use
|
||||
|
||||
# Generate tickets for every transport
|
||||
data = {
|
||||
'type': 'F',
|
||||
'service': svr.uuid,
|
||||
'user': request.user.uuid,
|
||||
'password': password
|
||||
}
|
||||
|
||||
ticket = TicketStore.create(data)
|
||||
|
||||
services.append({
|
||||
'id': 'F' + svr.uuid,
|
||||
'name': svr.name,
|
||||
@ -174,7 +192,6 @@ def index(request):
|
||||
'show_transports': svr.show_transports,
|
||||
'maintenance': svr.service.provider.maintenance_mode,
|
||||
'in_use': in_use,
|
||||
'ticket': ticket,
|
||||
})
|
||||
|
||||
logger.debug('Services: {0}'.format(services))
|
||||
@ -184,6 +201,7 @@ def index(request):
|
||||
if len(services) == 1 and GlobalConfig.AUTORUN_SERVICE.get(True) == '1' and len(services[0]['transports']) > 0:
|
||||
if request.session.get('autorunDone', '0') == '0':
|
||||
request.session['autorunDone'] = '1'
|
||||
# TODO: Make this to redirect to uds link directly
|
||||
return redirect('uds.web.views.service', idService=services[0]['id'], idTransport=services[0]['transports'][0]['id'])
|
||||
|
||||
response = render_to_response(
|
||||
|
@ -30,7 +30,7 @@
|
||||
'''
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__updated__ = '2015-03-16'
|
||||
__updated__ = '2015-03-23'
|
||||
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render_to_response
|
||||
@ -106,6 +106,55 @@ def service(request, idService, idTransport):
|
||||
return errors.exceptionView(request, e)
|
||||
|
||||
|
||||
@webLoginRequired(admin=False)
|
||||
def trans(request, idService, idTransport):
|
||||
kind, idService = idService[0], idService[1:]
|
||||
try:
|
||||
logger.debug('Kind of service: {0}, idService: {1}'.format(kind, idService))
|
||||
if kind == 'A': # This is an assigned service
|
||||
ads = UserService.objects.get(uuid=idService)
|
||||
else:
|
||||
ds = DeployedService.objects.get(uuid=idService)
|
||||
# We first do a sanity check for this, if the user has access to this service
|
||||
# If it fails, will raise an exception
|
||||
ds.validateUser(request.user)
|
||||
# Now we have to locate an instance of the service, so we can assign it to user.
|
||||
ads = UserServiceManager.manager().getAssignationForUser(ds, request.user)
|
||||
|
||||
if ads.isInMaintenance() is True:
|
||||
raise ServiceInMaintenanceMode()
|
||||
|
||||
logger.debug('Found service: {0}'.format(ads))
|
||||
trans = Transport.objects.get(uuid=idTransport)
|
||||
# Test if the service is ready
|
||||
if ads.isReady():
|
||||
log.doLog(ads, log.INFO, "User {0} from {1} has initiated access".format(request.user.name, request.ip), log.WEB)
|
||||
# If ready, show transport for this service, if also ready ofc
|
||||
iads = ads.getInstance()
|
||||
ip = iads.getIp()
|
||||
events.addEvent(ads.deployed_service, events.ET_ACCESS, username=request.user.name, srcip=request.ip, dstip=ip, uniqueid=ads.unique_id)
|
||||
if ip is not None:
|
||||
itrans = trans.getInstance()
|
||||
if itrans.isAvailableFor(ip):
|
||||
ads.setConnectionSource(request.ip, 'unknown')
|
||||
log.doLog(ads, log.INFO, "User service ready, rendering transport", log.WEB)
|
||||
|
||||
UserServiceManager.manager().notifyPreconnect(ads, itrans.processedUser(ads, request.user), itrans.protocol)
|
||||
return itrans.getLink(ads, trans, ip, request.os, request.user, webPassword(request), request)
|
||||
else:
|
||||
log.doLog(ads, log.WARN, "User service is not accessible (ip {0})".format(ip), log.TRANSPORT)
|
||||
logger.debug('Transport is not ready for user service {0}'.format(ads))
|
||||
else:
|
||||
logger.debug('Ip not available from user service {0}'.format(ads))
|
||||
else:
|
||||
log.doLog(ads, log.WARN, "User {0} from {1} tried to access, but machine was not ready".format(request.user.name, request.ip), log.WEB)
|
||||
# Not ready, show message and return to this page in a while
|
||||
return render_to_response(theme.template('service_not_ready.html'), context_instance=RequestContext(request))
|
||||
except Exception, e:
|
||||
logger.exception("Exception")
|
||||
return errors.exceptionView(request, e)
|
||||
|
||||
|
||||
@webLoginRequired(admin=False)
|
||||
def transcomp(request, idTransport, componentId):
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user