|
39 |
import os |
39 |
import os |
40 |
import time |
40 |
import time |
41 |
import json |
41 |
import json |
|
|
42 |
import tempfile |
43 |
import os.path |
42 |
|
44 |
|
43 |
import notifier |
45 |
import notifier |
44 |
import notifier.signals as signals |
46 |
import notifier.signals as signals |
|
70 |
|
72 |
|
71 |
TEMPUPLOADDIR = '/var/tmp/univention-management-console-frontend' |
73 |
TEMPUPLOADDIR = '/var/tmp/univention-management-console-frontend' |
72 |
|
74 |
|
|
|
75 |
class UploadManager(dict): |
76 |
def add(self, request_id, data): |
77 |
tmpfile = tempfile.NamedTemporaryFile(prefix=request_id, dir=TEMPUPLOADDIR, delete=False) |
78 |
tmpfile.write(data) |
79 |
tmpfile.close() |
80 |
if request_id in self: |
81 |
self[request_id].append(tmpfile.name) |
82 |
else: |
83 |
self[request_id] = [tmpfile.name] |
84 |
|
85 |
return tmpfile.name |
86 |
|
87 |
def cleanup(self, request_id): |
88 |
if request_id in self: |
89 |
filenames = self[request_id] |
90 |
for filename in filenames: |
91 |
if os.path.isfile(filename): |
92 |
os.unlink(filename) |
93 |
del self[request_id] |
94 |
return True |
95 |
|
96 |
return False |
97 |
|
98 |
_upload_manager = UploadManager() |
99 |
|
73 |
class State( signals.Provider ): |
100 |
class State( signals.Provider ): |
74 |
"""Holds information about the state of an active session |
101 |
"""Holds information about the state of an active session |
75 |
|
102 |
|
|
165 |
if msg.command == 'EXIT' and 'internal' in msg.arguments: |
192 |
if msg.command == 'EXIT' and 'internal' in msg.arguments: |
166 |
return |
193 |
return |
167 |
|
194 |
|
|
|
195 |
if msg.command == 'UPLOAD': |
196 |
# remove temporary uploaded files |
197 |
_upload_manager.cleanup(msg.id) |
198 |
|
168 |
self.signal_emit( 'result', msg ) |
199 |
self.signal_emit( 'result', msg ) |
169 |
|
200 |
|
170 |
def pid( self ): |
201 |
def pid( self ): |
|
522 |
module._inactivity_counter = MODULE_INACTIVITY_TIMER |
553 |
module._inactivity_counter = MODULE_INACTIVITY_TIMER |
523 |
|
554 |
|
524 |
def handle_request_upload( self, msg ): |
555 |
def handle_request_upload( self, msg ): |
525 |
"""Handles an UPLOAD request. The command is used for the HTTP |
556 |
"""Handles an UPLOAD request. |
526 |
access to the UMC server. Incoming HTTP requests that send a |
557 |
The request options is a dictionary of dictionaries which |
527 |
list of files are passed on to the UMC server by storing the |
558 |
can either be treaten as files or as normal strings |
528 |
files in temporary files and passing the information about the |
|
|
529 |
files to the UMC server in the options of the request. The |
530 |
request options must be a list of dictionaries. Each dictionary |
531 |
must contain the following keys: |
532 |
|
559 |
|
|
|
560 |
|
533 |
* *filename* -- the original name of the file |
561 |
* *filename* -- the original name of the file |
534 |
* *name* -- name of the form field |
562 |
* *name* -- name of the form field |
535 |
* *tmpfile* -- filename of the temporary file |
563 |
* *content* -- the content of the file |
|
|
564 |
* *mimetype* -- the file mimetype |
565 |
* *enconding* -- encoding of the content if the content is encoded |
536 |
|
566 |
|
537 |
:param Request msg: UMCP request |
567 |
:param Request msg: UMCP request |
538 |
""" |
568 |
""" |
539 |
# request.options = ( { 'filename' : store.filename, 'name' : store.name, 'tmpfile' : tmpfile } ) |
|
|
540 |
|
569 |
|
541 |
if not isinstance( msg.options, ( list, tuple ) ): |
570 |
if isinstance(msg.options, list): |
|
|
571 |
# backward compatibility |
572 |
try: |
573 |
msg.options = dict((val['name'], val) for val in msg.options) |
574 |
except (TypeError, KeyError): |
575 |
raise InvalidOptionsError |
576 |
|
577 |
if not isinstance(msg.options, dict): |
542 |
raise InvalidOptionsError |
578 |
raise InvalidOptionsError |
543 |
|
579 |
|
544 |
for file_obj in msg.options: |
580 |
def sanitize_item(item): |
545 |
# check if required options exists and file_obj is a dict |
581 |
if not isinstance(item, dict): |
546 |
try: |
582 |
raise InvalidOptionsError |
547 |
tmpfilename, filename, name = file_obj['tmpfile'], file_obj['filename'], file_obj['name'] |
|
|
548 |
except: |
549 |
raise InvalidOptionsError('required options "tmpfile", "filename" or "name" is missing') |
550 |
|
583 |
|
551 |
# limit files to tmpdir |
584 |
res = dict((key, item.get(key, '')) for key in ('filename', 'name', 'content', 'mimetype')) |
552 |
if not os.path.realpath(tmpfilename).startswith(TEMPUPLOADDIR): |
585 |
# make sure some fields are strings |
553 |
raise InvalidOptionsError('invalid file: invalid path') |
586 |
if not all(isinstance(res[k], str) for k in ('filename', 'name', 'mimetype')): |
|
|
587 |
raise InvalidOptionsError |
554 |
|
588 |
|
555 |
# check if file exists |
589 |
# if the field is a file the content must be a string |
556 |
if not os.path.isfile( tmpfilename ): |
590 |
if res['filename'] and not isinstance(res['content'], str): |
557 |
raise InvalidOptionsError('invalid file: file does not exists') |
591 |
raise InvalidOptionsError |
558 |
|
592 |
|
559 |
# don't accept files bigger than umc/server/upload/max |
593 |
# remove the possibility of XSS and path overwriting |
560 |
st = os.stat( tmpfilename ) |
594 |
for c in '<>/': |
561 |
max_size = int( ucr.get( 'umc/server/upload/max', 64 ) ) * 1024 |
595 |
res['filename'] = res['filename'].replace(c, '_') |
562 |
if st.st_size > max_size: |
|
|
563 |
raise InvalidOptionsError('filesize is too large, maximum allowed filesize is %d' % (max_size,)) |
564 |
|
596 |
|
565 |
if msg.arguments and msg.arguments[0] not in ('', '/'): |
597 |
return res |
566 |
# The request has arguments, so it will be treaten as COMMAND |
598 |
|
567 |
self.handle_request_command(msg) |
599 |
if not msg.arguments or msg.arguments[0] in ('', '/'): |
|
|
600 |
# The request is an generic UPLOAD command ('/upload') |
601 |
result = {} |
602 |
for item in msg.options.values(): |
603 |
# read tmpfile and convert to base64 |
604 |
res = sanitize_item(item) |
605 |
name = res['name'] |
606 |
|
607 |
# bring content into base64 (!) |
608 |
if item.get('encoding') != 'base64': |
609 |
res['content'] = base64.encodestring(res['content']) |
610 |
result[name] = res |
611 |
|
612 |
response = Response( msg ) |
613 |
response.result = result |
614 |
response.status = SUCCESS |
615 |
|
616 |
self.signal_emit( 'response', response ) |
568 |
return |
617 |
return |
569 |
|
618 |
|
570 |
# The request is an generic UPLOAD command (/upload) |
619 |
ucr.load() |
571 |
result = [] |
620 |
max_upload_size = int(ucr.get('umc/server/upload/max', 64)) * 1024 |
572 |
for file_obj in msg.options: |
621 |
min_free_space = int(ucr.get('umc/server/upload/min_free_space', 51200)) # kilobyte |
573 |
# read tmpfile and convert to base64 |
|
|
574 |
tmpfilename, filename, name = file_obj['tmpfile'], file_obj['filename'], file_obj['name'] |
575 |
with open( tmpfilename ) as buf: |
576 |
b64buf = base64.b64encode( buf.read() ) |
577 |
result.append( { 'filename' : filename, 'name' : name, 'content' : b64buf } ) |
578 |
|
622 |
|
579 |
response = Response( msg ) |
623 |
# check if enough free space is available |
580 |
response.result = result |
624 |
s = os.statvfs(TEMPUPLOADDIR) |
581 |
response.status = SUCCESS |
625 |
free_disk_space = s.f_bavail * s.f_frsize / 1024 # kilobyte |
|
|
626 |
if free_disk_space < min_free_space: |
627 |
CORE.error('There is not enough free space on disk to upload files') |
628 |
raise InvalidOptionsError('There is not enough free space on disk to upload files') |
582 |
|
629 |
|
583 |
self.signal_emit( 'response', response ) |
630 |
options = {} |
|
|
631 |
for item in msg.options.values(): |
632 |
res = sanitize_item(item) |
633 |
name, filename = res['name'], res['filename'] |
584 |
|
634 |
|
|
|
635 |
if not filename: |
636 |
# field is not a file, it's a string |
637 |
res = res['content'] |
638 |
else: |
639 |
content = res.pop('content') |
640 |
|
641 |
# don't accept files bigger than umc/server/upload/max |
642 |
clen = len(content) |
643 |
if clen > max_upload_size: |
644 |
CORE.warn('File of size %d could not be uploaded' % (clen)) |
645 |
raise InvalidOptionsError('Filesize is too large, maximum allowed filesize is %d' % (max_upload_size,)) |
646 |
|
647 |
# decode the file |
648 |
if item.get('encoding') == 'base64': |
649 |
content = base64.decodestring(content) |
650 |
|
651 |
# write content to disk |
652 |
tmpfile = _upload_manager.add(msg.id, content) |
653 |
res['tmpfile'] = tmpfile |
654 |
|
655 |
options[name] = res |
656 |
msg.options = options |
657 |
|
658 |
self.handle_request_command(msg) |
659 |
|
585 |
def handle_request_command( self, msg ): |
660 |
def handle_request_command( self, msg ): |
586 |
"""Handles a COMMAND request. The request must contain a valid |
661 |
"""Handles a COMMAND request. The request must contain a valid |
587 |
and known command that can be accessed by the current user. If |
662 |
and known command that can be accessed by the current user. If |