Exemple #1
0
def main(client_id, user_arguments_dict, environ=None):
    """Main function used by front end"""

    if environ is None:
        environ = os.environ

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id, op_header=False,
                                  op_menu=client_id)
    client_dir = client_id_dir(client_id)
    status = returnvalues.OK
    defaults = signature()[1]
    (validate_status, accepted) = validate_input(
        user_arguments_dict,
        defaults,
        output_objects,
        allow_rejects=False,
        # NOTE: path cannot use wildcards here
        typecheck_overrides={},
    )
    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    flags = ''.join(accepted['flags'])
    patterns = accepted['path']
    current_dir = accepted['current_dir'][-1]
    share_id = accepted['share_id'][-1]

    if not safe_handler(configuration, 'post', op_name, client_id,
                        get_csrf_limit(configuration), accepted):
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            '''Only accepting
CSRF-filtered POST requests to prevent unintended updates'''
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    # Either authenticated user client_id set or sharelink ID
    if client_id:
        user_id = client_id
        target_dir = client_id_dir(client_id)
        base_dir = configuration.user_home
        id_query = ''
        page_title = 'Create User Directory'
        userstyle = True
        widgets = True
    elif share_id:
        try:
            (share_mode, _) = extract_mode_id(configuration, share_id)
        except ValueError as err:
            logger.error('%s called with invalid share_id %s: %s' %
                         (op_name, share_id, err))
            output_objects.append({
                'object_type': 'error_text',
                'text': 'Invalid sharelink ID: %s' % share_id
            })
            return (output_objects, returnvalues.CLIENT_ERROR)
        # TODO: load and check sharelink pickle (currently requires client_id)
        user_id = 'anonymous user through share ID %s' % share_id
        if share_mode == 'read-only':
            logger.error('%s called without write access: %s' %
                         (op_name, accepted))
            output_objects.append({
                'object_type': 'error_text',
                'text': 'No write access!'
            })
            return (output_objects, returnvalues.CLIENT_ERROR)
        target_dir = os.path.join(share_mode, share_id)
        base_dir = configuration.sharelink_home
        id_query = '?share_id=%s' % share_id
        page_title = 'Create Shared Directory'
        userstyle = False
        widgets = False
    else:
        logger.error('%s called without proper auth: %s' % (op_name, accepted))
        output_objects.append({
            'object_type': 'error_text',
            'text': 'Authentication is missing!'
        })
        return (output_objects, returnvalues.SYSTEM_ERROR)

    # Please note that base_dir must end in slash to avoid access to other
    # user dirs when own name is a prefix of another user name

    base_dir = os.path.abspath(os.path.join(base_dir, target_dir)) + os.sep

    title_entry = find_entry(output_objects, 'title')
    title_entry['text'] = page_title
    title_entry['skipwidgets'] = not widgets
    title_entry['skipuserstyle'] = not userstyle
    output_objects.append({'object_type': 'header', 'text': page_title})

    # Input validation assures target_dir can't escape base_dir
    if not os.path.isdir(base_dir):
        output_objects.append({
            'object_type': 'error_text',
            'text': 'Invalid client/sharelink id!'
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    if verbose(flags):
        for flag in flags:
            output_objects.append({
                'object_type': 'text',
                'text': '%s using flag: %s' % (op_name, flag)
            })

    for pattern in patterns:

        # Check directory traversal attempts before actual handling to avoid
        # leaking information about file system layout while allowing
        # consistent error messages
        # NB: Globbing disabled on purpose here

        unfiltered_match = [base_dir + os.sep + current_dir + os.sep + pattern]
        match = []
        for server_path in unfiltered_match:
            # IMPORTANT: path must be expanded to abs for proper chrooting
            abs_path = os.path.abspath(server_path)
            if not valid_user_path(configuration, abs_path, base_dir, True):

                # out of bounds - save user warning for later to allow
                # partial match:
                # ../*/* is technically allowed to match own files.

                logger.warn('%s tried to %s %s restricted path! (%s)' %
                            (client_id, op_name, abs_path, pattern))
                continue
            match.append(abs_path)

        # Now actually treat list of allowed matchings and notify if no
        # (allowed) match

        if not match:
            output_objects.append({
                'object_type':
                'error_text',
                'text':
                "%s: cannot create directory '%s': Permission denied" %
                (op_name, pattern)
            })
            status = returnvalues.CLIENT_ERROR

        for abs_path in match:
            relative_path = abs_path.replace(base_dir, '')
            if verbose(flags):
                output_objects.append({
                    'object_type': 'file',
                    'name': relative_path
                })
            if not parents(flags) and os.path.exists(abs_path):
                output_objects.append({
                    'object_type': 'error_text',
                    'text': '%s: path exist!' % pattern
                })
                status = returnvalues.CLIENT_ERROR
                continue
            if not check_write_access(abs_path, parent_dir=True):
                logger.warning('%s called without write access: %s' %
                               (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    'cannot create "%s": inside a read-only location!' %
                    pattern
                })
                status = returnvalues.CLIENT_ERROR
                continue
            try:
                gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'],
                          'created', [relative_path])
                if parents(flags):
                    if not os.path.isdir(abs_path):
                        os.makedirs(abs_path)
                else:
                    os.mkdir(abs_path)
                logger.info('%s %s done' % (op_name, abs_path))
            except Exception as exc:
                if not isinstance(exc, GDPIOLogError):
                    gdp_iolog(configuration,
                              client_id,
                              environ['REMOTE_ADDR'],
                              'created', [relative_path],
                              failed=True,
                              details=exc)
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "%s: '%s' failed!" % (op_name, relative_path)
                })
                logger.error("%s: failed on '%s': %s" %
                             (op_name, relative_path, exc))

                status = returnvalues.SYSTEM_ERROR
                continue
            output_objects.append({
                'object_type':
                'text',
                'text':
                "created directory %s" % (relative_path)
            })
            if id_query:
                open_query = "%s;current_dir=%s" % (id_query, relative_path)
            else:
                open_query = "?current_dir=%s" % relative_path
            output_objects.append({
                'object_type': 'link',
                'destination': 'ls.py%s' % open_query,
                'text': 'Open %s' % relative_path
            })
            output_objects.append({'object_type': 'text', 'text': ''})

    output_objects.append({
        'object_type': 'link',
        'destination': 'ls.py%s' % id_query,
        'text': 'Return to files overview'
    })
    return (output_objects, status)
Exemple #2
0
def main(client_id, user_arguments_dict, environ=None):
    """Main function used by front end"""

    if environ is None:
        environ = os.environ

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id, op_header=False,
                                  op_menu=client_id)
    client_dir = client_id_dir(client_id)
    status = returnvalues.OK
    defaults = signature()[1]
    (validate_status, accepted) = validate_input(
        user_arguments_dict,
        defaults,
        output_objects,
        allow_rejects=False,
        # NOTE: path can use wildcards
        typecheck_overrides={'path': valid_path_pattern},
    )
    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    flags = ''.join(accepted['flags'])
    pattern_list = accepted['path']
    iosessionid = accepted['iosessionid'][-1]
    share_id = accepted['share_id'][-1]

    if not safe_handler(configuration, 'post', op_name, client_id,
                        get_csrf_limit(configuration), accepted):
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            '''Only accepting
CSRF-filtered POST requests to prevent unintended updates'''
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    # Either authenticated user client_id set or sharelink ID
    if client_id:
        user_id = client_id
        target_dir = client_id_dir(client_id)
        base_dir = configuration.user_home
        id_query = ''
        page_title = 'Remove User File'
        if force(flags):
            rm_helper = delete_path
        else:
            rm_helper = remove_path
        userstyle = True
        widgets = True
    elif share_id:
        try:
            (share_mode, _) = extract_mode_id(configuration, share_id)
        except ValueError as err:
            logger.error('%s called with invalid share_id %s: %s' %
                         (op_name, share_id, err))
            output_objects.append({
                'object_type': 'error_text',
                'text': 'Invalid sharelink ID: %s' % share_id
            })
            return (output_objects, returnvalues.CLIENT_ERROR)
        # TODO: load and check sharelink pickle (currently requires client_id)
        user_id = 'anonymous user through share ID %s' % share_id
        if share_mode == 'read-only':
            logger.error('%s called without write access: %s' %
                         (op_name, accepted))
            output_objects.append({
                'object_type': 'error_text',
                'text': 'No write access!'
            })
            return (output_objects, returnvalues.CLIENT_ERROR)
        target_dir = os.path.join(share_mode, share_id)
        base_dir = configuration.sharelink_home
        id_query = '?share_id=%s' % share_id
        page_title = 'Remove Shared File'
        rm_helper = delete_path
        userstyle = False
        widgets = False
    elif iosessionid.strip() and iosessionid.isalnum():
        user_id = iosessionid
        base_dir = configuration.webserver_home
        target_dir = iosessionid
        page_title = 'Remove Session File'
        rm_helper = delete_path
        userstyle = False
        widgets = False
    else:
        logger.error('%s called without proper auth: %s' % (op_name, accepted))
        output_objects.append({
            'object_type': 'error_text',
            'text': 'Authentication is missing!'
        })
        return (output_objects, returnvalues.SYSTEM_ERROR)

    # Please note that base_dir must end in slash to avoid access to other
    # user dirs when own name is a prefix of another user name

    base_dir = os.path.abspath(os.path.join(base_dir, target_dir)) + os.sep

    title_entry = find_entry(output_objects, 'title')
    title_entry['text'] = page_title
    title_entry['skipwidgets'] = not widgets
    title_entry['skipuserstyle'] = not userstyle
    output_objects.append({'object_type': 'header', 'text': page_title})

    logger.debug("%s: with paths: %s" % (op_name, pattern_list))

    # Input validation assures target_dir can't escape base_dir
    if not os.path.isdir(base_dir):
        output_objects.append({
            'object_type': 'error_text',
            'text': 'Invalid client/sharelink/session id!'
        })
        logger.warning('%s used %s with invalid base dir: %s' %
                       (user_id, op_name, base_dir))
        return (output_objects, returnvalues.CLIENT_ERROR)

    if verbose(flags):
        for flag in flags:
            output_objects.append({
                'object_type': 'text',
                'text': '%s using flag: %s' % (op_name, flag)
            })

    for pattern in pattern_list:

        # Check directory traversal attempts before actual handling to avoid
        # leaking information about file system layout while allowing
        # consistent error messages

        unfiltered_match = glob.glob(base_dir + pattern)
        match = []
        for server_path in unfiltered_match:
            # IMPORTANT: path must be expanded to abs for proper chrooting
            abs_path = os.path.abspath(server_path)
            if not valid_user_path(configuration, abs_path, base_dir, True):

                # out of bounds - save user warning for later to allow
                # partial match:
                # ../*/* is technically allowed to match own files.

                logger.warning('%s tried to %s restricted path %s ! ( %s)' %
                               (client_id, op_name, abs_path, pattern))
                continue
            match.append(abs_path)

        # Now actually treat list of allowed matchings and notify if no
        # (allowed) match

        if not match:
            logger.warning("%s: no matching paths: %s" %
                           (op_name, pattern_list))
            output_objects.append({
                'object_type': 'file_not_found',
                'name': pattern
            })
            status = returnvalues.FILE_NOT_FOUND

        for abs_path in match:
            real_path = os.path.realpath(abs_path)
            relative_path = abs_path.replace(base_dir, '')
            if verbose(flags):
                output_objects.append({
                    'object_type': 'file',
                    'name': relative_path
                })

            # Make it harder to accidentially delete too much - e.g. do not
            # delete VGrid files without explicit selection of subdir contents

            if abs_path == os.path.abspath(base_dir):
                logger.error("%s: refusing rm home dir: %s" %
                             (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'warning',
                    'text':
                    "You're not allowed to delete your entire home directory!"
                })
                status = returnvalues.CLIENT_ERROR
                continue
            # Generally refuse handling symlinks including root vgrid shares
            elif os.path.islink(abs_path):
                logger.error("%s: refusing rm link: %s" % (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'warning',
                    'text':
                    """
You're not allowed to delete entire special folders like %s shares and %s
""" % (configuration.site_vgrid_label, trash_linkname)
                })
                status = returnvalues.CLIENT_ERROR
                continue
            # Additionally refuse operations on inherited subvgrid share roots
            elif in_vgrid_share(configuration, abs_path) == relative_path:
                output_objects.append({
                    'object_type':
                    'warning',
                    'text':
                    """You're not allowed to
remove entire %s shared folders!""" % configuration.site_vgrid_label
                })
                status = returnvalues.CLIENT_ERROR
                continue
            elif os.path.isdir(abs_path) and not recursive(flags):
                logger.error("%s: non-recursive call on dir '%s'" %
                             (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "cannot remove '%s': is a direcory" % relative_path
                })
                status = returnvalues.CLIENT_ERROR
                continue
            trash_base = get_trash_location(configuration, abs_path)
            if not trash_base and not force(flags):
                logger.error("%s: no trash for dir '%s'" % (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "No trash enabled for '%s' - read-only?" % relative_path
                })
                status = returnvalues.CLIENT_ERROR
                continue
            try:
                if rm_helper == remove_path and \
                    os.path.commonprefix([real_path, trash_base]) \
                        == trash_base:
                    logger.warning("%s: already in trash: '%s'" %
                                   (op_name, real_path))
                    output_objects.append({
                        'object_type':
                        'error_text',
                        'text':
                        """
'%s' is already in trash - no action: use force flag to permanently delete""" %
                        relative_path
                    })
                    status = returnvalues.CLIENT_ERROR
                    continue
            except Exception as err:
                logger.error("%s: check trash failed: %s" % (op_name, err))
                continue
            if not check_write_access(abs_path):
                logger.warning('%s called without write access: %s' %
                               (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    'cannot remove "%s": inside a read-only location!' %
                    pattern
                })
                status = returnvalues.CLIENT_ERROR
                continue

            # TODO: limit delete in vgrid share trash to vgrid owners / conf?
            #       ... malicious members can still e.g. truncate all files.
            #       we could consider removing write bit on move to trash.
            # TODO: user setting to switch on/off trash?
            # TODO: add direct delete checkbox in fileman move to trash dialog?
            # TODO: add empty trash option for Trash?
            # TODO: user settings to define read-only and auto-expire in trash?
            # TODO: add trash support for sftp/ftps/webdavs?

            gdp_iolog_action = 'deleted'
            gdp_iolog_paths = [relative_path]
            if rm_helper == remove_path:
                gdp_iolog_action = 'moved'
                trash_base_path = \
                    get_trash_location(configuration, abs_path, True)
                trash_relative_path = \
                    trash_base_path.replace(configuration.user_home, '')
                trash_relative_path = \
                    trash_relative_path.replace(
                        configuration.vgrid_files_home, '')
                gdp_iolog_paths.append(trash_relative_path)
            try:
                gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'],
                          gdp_iolog_action, gdp_iolog_paths)
                gdp_iolog_status = True
            except GDPIOLogError as exc:
                gdp_iolog_status = False
                rm_err = [str(exc)]
            rm_status = False
            if gdp_iolog_status:
                (rm_status, rm_err) = rm_helper(configuration, abs_path)
            if not rm_status or not gdp_iolog_status:
                if gdp_iolog_status:
                    gdp_iolog(configuration,
                              client_id,
                              environ['REMOTE_ADDR'],
                              gdp_iolog_action,
                              gdp_iolog_paths,
                              failed=True,
                              details=rm_err)
                logger.error("%s: failed on '%s': %s" %
                             (op_name, abs_path, ', '.join(rm_err)))
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "remove '%s' failed: %s" %
                    (relative_path, '. '.join(rm_err))
                })
                status = returnvalues.SYSTEM_ERROR
                continue
            logger.info("%s: successfully (re)moved %s" % (op_name, abs_path))
            output_objects.append({
                'object_type': 'text',
                'text': "removed %s" % (relative_path)
            })

    output_objects.append({
        'object_type': 'link',
        'destination': 'ls.py%s' % id_query,
        'text': 'Return to files overview'
    })
    return (output_objects, status)
Exemple #3
0
def main(client_id, user_arguments_dict, environ=None):
    """Main function used by front end"""

    if environ is None:
        environ = os.environ

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id)
    client_dir = client_id_dir(client_id)
    defaults = signature()[1]
    status = returnvalues.OK
    (validate_status, accepted) = validate_input_and_cert(
        user_arguments_dict,
        defaults,
        output_objects,
        client_id,
        configuration,
        allow_rejects=False,
        # NOTE: path can use wildcards, dst cannot
        typecheck_overrides={'path': valid_path_pattern},
    )
    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    flags = ''.join(accepted['flags'])
    patterns = accepted['path']
    dst = accepted['dst'][-1].lstrip(os.sep)

    # Please note that base_dir must end in slash to avoid access to other
    # user dirs when own name is a prefix of another user name

    base_dir = os.path.abspath(
        os.path.join(configuration.user_home, client_dir)) + os.sep

    if verbose(flags):
        for flag in flags:
            output_objects.append({
                'object_type': 'text',
                'text': '%s using flag: %s' % (op_name, flag)
            })
    if dst:
        if not safe_handler(configuration, 'post', op_name, client_id,
                            get_csrf_limit(configuration), accepted):
            output_objects.append({
                'object_type':
                'error_text',
                'text':
                '''Only accepting
CSRF-filtered POST requests to prevent unintended updates'''
            })
            return (output_objects, returnvalues.CLIENT_ERROR)

        dst_mode = "wb"
        # IMPORTANT: path must be expanded to abs for proper chrooting
        abs_dest = os.path.abspath(os.path.join(base_dir, dst))
        relative_dst = abs_dest.replace(base_dir, '')
        if not valid_user_path(configuration, abs_dest, base_dir, True):
            logger.warning('%s tried to %s into restricted path %s ! (%s)' %
                           (client_id, op_name, abs_dest, dst))
            output_objects.append({
                'object_type': 'error_text',
                'text': "invalid destination: '%s'" % dst
            })
            return (output_objects, returnvalues.CLIENT_ERROR)

    for pattern in patterns:

        # Check directory traversal attempts before actual handling to avoid
        # leaking information about file system layout while allowing
        # consistent error messages

        unfiltered_match = glob.glob(base_dir + pattern)
        match = []
        for server_path in unfiltered_match:
            # IMPORTANT: path must be expanded to abs for proper chrooting
            abs_path = os.path.abspath(server_path)
            if not valid_user_path(configuration, abs_path, base_dir, True):

                # out of bounds - save user warning for later to allow
                # partial match:
                # ../*/* is technically allowed to match own files.

                logger.warning('%s tried to %s restricted path %s ! (%s)' %
                               (client_id, op_name, abs_path, pattern))
                continue
            match.append(abs_path)

        # Now actually treat list of allowed matchings and notify if no
        # (allowed) match

        if not match:
            output_objects.append({
                'object_type': 'file_not_found',
                'name': pattern
            })
            status = returnvalues.FILE_NOT_FOUND

        for abs_path in match:
            output_lines = []
            relative_path = abs_path.replace(base_dir, '')
            try:
                gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'],
                          'accessed', [relative_path])
                fd = open(abs_path, 'r')

                # use file directly as iterator for efficiency

                for line in fd:
                    output_lines.append(line)
                fd.close()
            except Exception as exc:
                if not isinstance(exc, GDPIOLogError):
                    gdp_iolog(configuration,
                              client_id,
                              environ['REMOTE_ADDR'],
                              'accessed', [relative_path],
                              failed=True,
                              details=exc)
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "%s: '%s': %s" % (op_name, relative_path, exc)
                })
                logger.error("%s: failed on '%s': %s" %
                             (op_name, relative_path, exc))

                status = returnvalues.SYSTEM_ERROR
                continue
            if dst:
                try:
                    gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'],
                              'modified', [dst])
                    out_fd = open(abs_dest, dst_mode)
                    out_fd.writelines(output_lines)
                    out_fd.close()
                    logger.info('%s %s %s done' %
                                (op_name, abs_path, abs_dest))
                except Exception as exc:
                    if not isinstance(exc, GDPIOLogError):
                        gdp_iolog(configuration,
                                  client_id,
                                  environ['REMOTE_ADDR'],
                                  'modified', [dst],
                                  failed=True,
                                  details=exc)
                    output_objects.append({
                        'object_type': 'error_text',
                        'text': "write failed: '%s'" % exc
                    })
                    logger.error("%s: write failed on '%s': %s" %
                                 (op_name, abs_dest, exc))
                    status = returnvalues.SYSTEM_ERROR
                    continue
                output_objects.append({
                    'object_type':
                    'text',
                    'text':
                    "wrote %s to %s" % (relative_path, relative_dst)
                })
                # Prevent truncate after first write
                dst_mode = "ab+"
            else:
                entry = {
                    'object_type': 'file_output',
                    'lines': output_lines,
                    'wrap_binary': binary(flags),
                    'wrap_targets': ['lines']
                }
                if verbose(flags):
                    entry['path'] = relative_path
                output_objects.append(entry)

                # TODO: rip this hack out into real download handler?
                # Force download of files when output_format == 'file_format'
                # This will only work for the first file matching a glob when
                # using file_format.
                # And it is supposed to only work for one file.
                if 'output_format' in user_arguments_dict:
                    output_format = user_arguments_dict['output_format'][0]
                    if output_format == 'file':
                        output_objects.append({
                            'object_type':
                            'start',
                            'headers': [('Content-Disposition',
                                         'attachment; filename="%s";' %
                                         os.path.basename(abs_path))]
                        })

    return (output_objects, status)
Exemple #4
0
def main(client_id, user_arguments_dict, environ=None):
    """Main function used by front end"""

    if environ is None:
        environ = os.environ

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id, op_header=False,
                                  op_menu=client_id)
    defaults = signature()[1]
    (validate_status, accepted) = validate_input(
        user_arguments_dict,
        defaults,
        output_objects,
        allow_rejects=False,
        # NOTE: path can use wildcards, current_dir cannot
        typecheck_overrides={'path': valid_path_pattern},
    )
    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    flags = ''.join(accepted['flags'])
    pattern_list = accepted['path']
    current_dir = accepted['current_dir'][-1].lstrip('/')
    share_id = accepted['share_id'][-1]

    status = returnvalues.OK

    read_mode, write_mode = True, True
    # Either authenticated user client_id set or sharelink ID
    if client_id:
        user_id = client_id
        target_dir = client_id_dir(client_id)
        base_dir = configuration.user_home
        redirect_name = configuration.site_user_redirect
        redirect_path = redirect_name
        id_args = ''
        root_link_name = 'USER HOME'
        main_class = "user_ls"
        page_title = 'User Files'
        userstyle = True
        widgets = True
        visibility_mods = '''
            .%(main_class)s .disable_read { display: none; }
            .%(main_class)s .disable_write { display: none; }
            '''
    elif share_id:
        try:
            (share_mode, _) = extract_mode_id(configuration, share_id)
        except ValueError as err:
            logger.error('%s called with invalid share_id %s: %s' %
                         (op_name, share_id, err))
            output_objects.append({
                'object_type': 'error_text',
                'text': 'Invalid sharelink ID: %s' % share_id
            })
            return (output_objects, returnvalues.CLIENT_ERROR)
        # TODO: load and check sharelink pickle (currently requires client_id)
        # then include shared by %(owner)s on page header
        user_id = 'anonymous user through share ID %s' % share_id
        target_dir = os.path.join(share_mode, share_id)
        base_dir = configuration.sharelink_home
        redirect_name = 'share_redirect'
        redirect_path = os.path.join(redirect_name, share_id)
        id_args = 'share_id=%s;' % share_id
        root_link_name = '%s' % share_id
        main_class = "sharelink_ls"
        page_title = 'Shared Files'
        userstyle = False
        widgets = False
        # default to include file info
        if flags == '':
            flags += 'f'
        if share_mode == 'read-only':
            write_mode = False
            visibility_mods = '''
            .%(main_class)s .enable_write { display: none; }
            .%(main_class)s .disable_read { display: none; }
            '''
        elif share_mode == 'write-only':
            read_mode = False
            visibility_mods = '''
            .%(main_class)s .enable_read { display: none; }
            .%(main_class)s .disable_write { display: none; }
            '''
        else:
            visibility_mods = '''
            .%(main_class)s .disable_read { display: none; }
            .%(main_class)s .disable_write { display: none; }
            '''
    else:
        logger.error('%s called without proper auth: %s' % (op_name, accepted))
        output_objects.append({
            'object_type': 'error_text',
            'text': 'Authentication is missing!'
        })
        return (output_objects, returnvalues.SYSTEM_ERROR)

    visibility_toggle = '''
        <style>
        %s
        </style>
        ''' % (visibility_mods % {
        'main_class': main_class
    })

    # Please note that base_dir must end in slash to avoid access to other
    # user dirs when own name is a prefix of another user name

    base_dir = os.path.abspath(os.path.join(base_dir, target_dir)) + os.sep

    if not os.path.isdir(base_dir):
        logger.error('%s called on missing base_dir: %s' % (op_name, base_dir))
        output_objects.append({
            'object_type': 'error_text',
            'text': 'No such %s!' % page_title.lower()
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    title_entry = find_entry(output_objects, 'title')
    title_entry['text'] = page_title
    title_entry['skipwidgets'] = not widgets
    title_entry['skipuserstyle'] = not userstyle
    user_settings = title_entry.get('user_settings', {})

    open_button_id = 'open_fancy_upload'
    form_method = 'post'
    csrf_limit = get_csrf_limit(configuration)
    fill_helpers = {
        'dest_dir': current_dir + os.sep,
        'share_id': share_id,
        'flags': flags,
        'tmp_flags': flags,
        'long_set': long_list(flags),
        'recursive_set': recursive(flags),
        'all_set': all(flags),
        'fancy_open': open_button_id,
        'fancy_dialog': fancy_upload_html(configuration),
        'form_method': form_method,
        'csrf_field': csrf_field,
        'csrf_limit': csrf_limit
    }
    target_op = 'uploadchunked'
    csrf_token = make_csrf_token(configuration, form_method, target_op,
                                 client_id, csrf_limit)
    fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token})
    (cf_import, cf_init, cf_ready) = confirm_js(configuration)
    (fu_import, fu_init,
     fu_ready) = fancy_upload_js(configuration,
                                 'function() { location.reload(); }', share_id,
                                 csrf_token)
    add_import = '''
%s
%s
    ''' % (cf_import, fu_import)
    add_init = '''
%s
%s
%s
%s
    ''' % (cf_init, fu_init, select_all_javascript(),
           selected_file_actions_javascript())
    add_ready = '''
%s
%s
    /* wrap openFancyUpload in function to avoid event data as argument */
    $("#%s").click(function() { openFancyUpload(); });
    $("#checkall_box").click(toggleChecked);
    ''' % (cf_ready, fu_ready, open_button_id)
    # TODO: can we update style inline to avoid explicit themed_styles?
    styles = themed_styles(
        configuration,
        advanced=['jquery.fileupload.css', 'jquery.fileupload-ui.css'],
        skin=['fileupload-ui.custom.css'],
        user_settings=user_settings)
    styles['advanced'] += '''
    %s
    ''' % visibility_toggle
    title_entry['style'] = styles
    title_entry['script']['advanced'] += add_import
    title_entry['script']['init'] += add_init
    title_entry['script']['ready'] += add_ready
    title_entry['script']['body'] = ' class="%s"' % main_class
    output_objects.append({'object_type': 'header', 'text': page_title})

    # TODO: move to output html handler
    output_objects.append({
        'object_type': 'html_form',
        'text': confirm_html(configuration)
    })

    # Shared URL helpers
    ls_url_template = 'ls.py?%scurrent_dir=%%(rel_dir_enc)s;flags=%s' % \
                      (id_args, flags)
    csrf_token = make_csrf_token(configuration, form_method, 'rm', client_id,
                                 csrf_limit)
    rm_url_template = 'rm.py?%spath=%%(rel_path_enc)s;%s=%s' % \
                      (id_args, csrf_field, csrf_token)
    rmdir_url_template = 'rm.py?%spath=%%(rel_path_enc)s;flags=r;%s=%s' % \
        (id_args, csrf_field, csrf_token)
    editor_url_template = 'editor.py?%spath=%%(rel_path_enc)s' % id_args
    redirect_url_template = '/%s/%%(rel_path_enc)s' % redirect_path

    location_pre_html = """
<div class='files'>
<table class='files'>
<tr class=title><td class=centertext>
Working directory:
</td></tr>
<tr><td class='centertext'>
"""
    output_objects.append({
        'object_type': 'html_form',
        'text': location_pre_html
    })
    # Use current_dir nav location links
    for pattern in pattern_list[:1]:
        links = []
        links.append({
            'object_type': 'link',
            'text': root_link_name,
            'destination': ls_url_template % {
                'rel_dir_enc': '.'
            }
        })
        prefix = ''
        parts = os.path.normpath(current_dir).split(os.sep)
        for i in parts:
            if i == ".":
                continue
            prefix = os.path.join(prefix, i)
            links.append({
                'object_type': 'link',
                'text': i,
                'destination': ls_url_template % {
                    'rel_dir_enc': quote(prefix)
                }
            })
        output_objects.append({
            'object_type': 'multilinkline',
            'links': links,
            'sep': ' %s ' % os.sep
        })
    location_post_html = """
</td></tr>
</table>
</div>
<br />
"""

    output_objects.append({
        'object_type': 'html_form',
        'text': location_post_html
    })

    more_html = """
<div class='files if_full'>
<form method='%(form_method)s' name='fileform' onSubmit='return selectedFilesAction();'>
<table class='files'>
<tr class=title><td class=centertext colspan=2>
Advanced file actions
</td></tr>
<tr><td>
Action on paths selected below
(please hold mouse cursor over button for a description):
</td>
<td class=centertext>
<input type='hidden' name='output_format' value='html' />
<input type='hidden' name='flags' value='v' />
<input type='submit' title='Show concatenated contents (cat)' onClick='document.pressed=this.value' value='cat' />
<input type='submit' onClick='document.pressed=this.value' value='head' title='Show first lines (head)' />
<input type='submit' onClick='document.pressed=this.value' value='tail' title='Show last lines (tail)' />
<input type='submit' onClick='document.pressed=this.value' value='wc' title='Count lines/words/chars (wc)' />
<input type='submit' onClick='document.pressed=this.value' value='stat' title='Show details (stat)' />
<input type='submit' onClick='document.pressed=this.value' value='touch' title='Update timestamp (touch)' />
<input type='submit' onClick='document.pressed=this.value' value='truncate' title='truncate! (truncate)' />
<input type='submit' onClick='document.pressed=this.value' value='rm' title='delete! (rm)' />
<input type='submit' onClick='document.pressed=this.value' value='rmdir' title='Remove directory (rmdir)' />
<input type='submit' onClick='document.pressed=this.value' value='submit' title='Submit file (submit)' />
</td></tr>
</table>    
</form>
</div>
""" % {
        'form_method': form_method
    }

    output_objects.append({'object_type': 'html_form', 'text': more_html})
    dir_listings = []
    output_objects.append({
        'object_type': 'dir_listings',
        'dir_listings': dir_listings,
        'flags': flags,
        'redirect_name': redirect_name,
        'redirect_path': redirect_path,
        'share_id': share_id,
        'ls_url_template': ls_url_template,
        'rm_url_template': rm_url_template,
        'rmdir_url_template': rmdir_url_template,
        'editor_url_template': editor_url_template,
        'redirect_url_template': redirect_url_template,
    })

    first_match = None
    for pattern in pattern_list:

        # Check directory traversal attempts before actual handling to avoid
        # leaking information about file system layout while allowing
        # consistent error messages

        current_path = os.path.normpath(os.path.join(base_dir, current_dir))
        unfiltered_match = glob.glob(current_path + os.sep + pattern)
        match = []
        for server_path in unfiltered_match:
            # IMPORTANT: path must be expanded to abs for proper chrooting
            abs_path = os.path.abspath(server_path)
            if not valid_user_path(configuration, abs_path, base_dir, True):
                logger.warning('%s tried to %s restricted path %s ! (%s)' %
                               (user_id, op_name, abs_path, pattern))
                continue
            match.append(abs_path)
            if not first_match:
                first_match = abs_path

        # Now actually treat list of allowed matchings and notify if no
        # (allowed) match

        if not match:
            output_objects.append({
                'object_type': 'file_not_found',
                'name': pattern
            })
            status = returnvalues.FILE_NOT_FOUND

        # Never show any ls output in write-only mode (css hide is not enough!)
        if not read_mode:
            continue

        for abs_path in match:
            if abs_path + os.sep == base_dir:
                relative_path = '.'
            else:
                relative_path = abs_path.replace(base_dir, '')
            entries = []
            dir_listing = {
                'object_type': 'dir_listing',
                'relative_path': relative_path,
                'entries': entries,
                'flags': flags,
            }
            try:
                gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'],
                          'accessed', [relative_path])
            except GDPIOLogError as exc:
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "%s: '%s': %s" % (op_name, relative_path, exc)
                })
                logger.error("%s: failed on '%s': %s" %
                             (op_name, relative_path, exc))
                continue
            handle_ls(configuration, output_objects, entries, base_dir,
                      abs_path, flags, 0)
            dir_listings.append(dir_listing)

    output_objects.append({
        'object_type':
        'html_form',
        'text':
        """<br/>
    <div class='files disable_read'>
    <p class='info icon'>"""
    })
    # Shared message for text (e.g. user scripts) and html-format
    if not read_mode:
        # Please note that we use verbatim to get info icon right in html
        output_objects.append({
            'object_type':
            'verbatim',
            'text':
            """
This is a write-only share so you do not have access to see the files, only
upload data and create directories.
    """
        })
    output_objects.append({
        'object_type':
        'html_form',
        'text':
        """
    </p>
    </div>
    <div class='files enable_read'>
    <form method='get' action='ls.py'>
    <table class='files'>
    <tr class=title><td class=centertext>
    Filter paths (wildcards like * and ? are allowed)
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    <input type='text' name='path' value='' />
    <input type='submit' value='Filter' />
    </td></tr>
    </table>    
    </form>
    </div>
    """ % fill_helpers
    })

    # Short/long format buttons

    fill_helpers['tmp_flags'] = flags + 'l'
    htmlform = """
    <table class='files if_full'>
    <tr class=title><td class=centertext colspan=4>
    File view options
    </td></tr>
    <tr><td colspan=4><br /></td></tr>
    <tr class=title><td>Parameter</td><td>Setting</td><td>Enable</td><td>Disable</td></tr>
    <tr><td>Long format</td><td>
    %(long_set)s</td><td>
    <form method='get' action='ls.py'>
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(tmp_flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    """ % fill_helpers

    for entry in pattern_list:
        htmlform += "<input type='hidden' name='path' value='%s' />" % entry
    fill_helpers['tmp_flags'] = flags.replace('l', '')
    htmlform += """
    <input type='submit' value='On' /><br />
    </form>
    </td><td>
    <form method='get' action='ls.py'>
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(tmp_flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    """ % fill_helpers
    for entry in pattern_list:
        htmlform += "<input type='hidden' name='path' value='%s' />" % entry
    htmlform += """
    <input type='submit' value='Off' /><br />
    </form>
    </td></tr>
    """

    # Recursive output

    fill_helpers['tmp_flags'] = flags + 'r'
    htmlform += """
    <!-- Non-/recursive list buttons -->
    <tr><td>Recursion</td><td>
    %(recursive_set)s</td><td>""" % fill_helpers
    htmlform += """
    <form method='get' action='ls.py'>
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(tmp_flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    """ % fill_helpers
    for entry in pattern_list:
        htmlform += "<input type='hidden' name='path' value='%s' />" % entry
    fill_helpers['tmp_flags'] = flags.replace('r', '')
    htmlform += """
    <input type='submit' value='On' /><br />
    </form>
    </td><td>
    <form method='get' action='ls.py'>
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(tmp_flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    """ % fill_helpers

    for entry in pattern_list:
        htmlform += "<input type='hidden' name='path' value='%s' />"\
                    % entry
        htmlform += """
    <input type='submit' value='Off' /><br />
    </form>
    </td></tr>
    """

    htmlform += """
    <!-- Show dot files buttons -->
    <tr><td>Show hidden files</td><td>
    %(all_set)s</td><td>""" % fill_helpers
    fill_helpers['tmp_flags'] = flags + 'a'
    htmlform += """
    <form method='get' action='ls.py'>
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(tmp_flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    """ % fill_helpers
    for entry in pattern_list:
        htmlform += "<input type='hidden' name='path' value='%s' />" % entry
    fill_helpers['tmp_flags'] = flags.replace('a', '')
    htmlform += """
    <input type='submit' value='On' /><br />
    </form>
    </td><td>
    <form method='get' action='ls.py'>
    <input type='hidden' name='output_format' value='html' />
    <input type='hidden' name='flags' value='%(tmp_flags)s' />
    <input type='hidden' name='share_id' value='%(share_id)s' />
    <input name='current_dir' type='hidden' value='%(dest_dir)s' />
    """ % fill_helpers
    for entry in pattern_list:
        htmlform += "<input type='hidden' name='path' value='%s' />" % entry
    htmlform += """
    <input type='submit' value='Off' /><br />
    </form>
    </td></tr>
    </table>
    """

    # show flag buttons after contents to limit clutter

    output_objects.append({'object_type': 'html_form', 'text': htmlform})

    # create additional action forms

    if first_match:
        htmlform = """
<br />
<div class='files disable_write'>
<p class='info icon'>
This is a read-only share so you do not have access to edit or add files, only
view data.
</p>
</div>
<table class='files enable_write if_full'>
<tr class=title><td class=centertext colspan=3>
Edit file
</td></tr>
<tr><td>
Fill in the path of a file to edit and press 'edit' to open that file in the<br />
online file editor. Alternatively a file can be selected for editing through<br />
the listing of personal files. 
</td><td colspan=2 class=righttext>
<form name='editor' method='get' action='editor.py'>
<input type='hidden' name='output_format' value='html' />
<input type='hidden' name='share_id' value='%(share_id)s' />
<input name='current_dir' type='hidden' value='%(dest_dir)s' />
<input type='text' name='path' size=50 value='' required />
<input type='submit' value='edit' />
</form>
</td></tr>
</table>
<br />""" % fill_helpers
        target_op = 'mkdir'
        csrf_token = make_csrf_token(configuration, form_method, target_op,
                                     client_id, csrf_limit)
        fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token})
        htmlform += """
<table class='files enable_write'>
<tr class=title><td class=centertext colspan=4>
Create directory
</td></tr>
<tr><td>
Name of new directory to be created in current directory (%(dest_dir)s)
</td><td class=righttext colspan=3>
<form method='%(form_method)s' action='%(target_op)s.py'>
<input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' />
<input type='hidden' name='share_id' value='%(share_id)s' />
<input name='current_dir' type='hidden' value='%(dest_dir)s' />
<input name='path' size=50 required />
<input type='submit' value='Create' name='mkdirbutton' />
</form>
</td></tr>
</table>
<br />
""" % fill_helpers
        target_op = 'textarea'
        csrf_token = make_csrf_token(configuration, form_method, target_op,
                                     client_id, csrf_limit)
        fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token})
        htmlform += """
<form enctype='multipart/form-data' method='%(form_method)s'
    action='%(target_op)s.py'>
<input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' />
<input type='hidden' name='share_id' value='%(share_id)s' />
<table class='files enable_write if_full'>
<tr class='title'><td class=centertext colspan=4>
Upload file
</td></tr>
<tr><td colspan=4>
Upload file to current directory (%(dest_dir)s)
</td></tr>
<tr class='if_full'><td colspan=2>
Extract package files (.zip, .tar.gz, .tar.bz2)
</td><td colspan=2>
<input type=checkbox name='extract_0' />
</td></tr>
<tr class='if_full'><td colspan=2>
Submit mRSL files (also .mRSL files included in packages)
</td><td colspan=2>
<input type=checkbox name='submitmrsl_0' />
</td></tr>
<tr><td>    
File to upload
</td><td class=righttext colspan=3>
<input name='fileupload_0_0_0' type='file'/>
</td></tr>
<tr><td>
Optional remote filename (extra useful in windows)
</td><td class=righttext colspan=3>
<input name='default_remotefilename_0' type='hidden' value='%(dest_dir)s'/>
<input name='remotefilename_0' type='text' size='50' value='%(dest_dir)s'/>
<input type='submit' value='Upload' name='sendfile'/>
</td></tr>
</table>
</form>
%(fancy_dialog)s
<table class='files enable_write'>
<tr class='title'><td class='centertext'>
Upload files efficiently (using chunking).
</td></tr>
<tr><td class='centertext'>
<button id='%(fancy_open)s'>Open Upload dialog</button>
</td></tr>
</table>
<script type='text/javascript' >
    setUploadDest('%(dest_dir)s');
</script>
    """ % fill_helpers
        output_objects.append({'object_type': 'html_form', 'text': htmlform})
    return (output_objects, status)
Exemple #5
0
def main(client_id, user_arguments_dict, environ=None):
    """Main function used by front end"""

    if environ is None:
        environ = os.environ

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id)
    client_dir = client_id_dir(client_id)
    status = returnvalues.OK
    defaults = signature()[1]
    (validate_status, accepted) = validate_input_and_cert(
        user_arguments_dict,
        defaults,
        output_objects,
        client_id,
        configuration,
        allow_rejects=False,
        # NOTE: src and dst can use wildcards here
        typecheck_overrides={
            'src': valid_path_pattern,
            'dst': valid_path_pattern
        },
    )
    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    flags = ''.join(accepted['flags'])
    src_list = accepted['src']
    dst = accepted['dst'][-1]

    if not safe_handler(configuration, 'post', op_name, client_id,
                        get_csrf_limit(configuration), accepted):
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            '''Only accepting
CSRF-filtered POST requests to prevent unintended updates'''
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    # Please note that base_dir must end in slash to avoid access to other
    # user dirs when own name is a prefix of another user name

    base_dir = os.path.abspath(
        os.path.join(configuration.user_home, client_dir)) + os.sep

    status = returnvalues.OK

    abs_dest = base_dir + dst
    dst_list = glob.glob(abs_dest)
    if not dst_list:

        # New destination?

        if not glob.glob(os.path.dirname(abs_dest)):
            output_objects.append({
                'object_type': 'error_text',
                'text': 'Illegal dst path provided!'
            })
            return (output_objects, returnvalues.CLIENT_ERROR)
        else:
            dst_list = [abs_dest]

    # Use last match in case of multiple matches

    dest = dst_list[-1]
    if len(dst_list) > 1:
        output_objects.append({
            'object_type':
            'warning',
            'text':
            'dst (%s) matches multiple targets - using last: %s' % (dst, dest)
        })

    # IMPORTANT: path must be expanded to abs for proper chrooting
    abs_dest = os.path.abspath(dest)

    # Don't use abs_path in output as it may expose underlying
    # fs layout.

    relative_dest = abs_dest.replace(base_dir, '')
    if not valid_user_path(configuration, abs_dest, base_dir, True):
        logger.warning('%s tried to %s to restricted path %s ! (%s)' %
                       (client_id, op_name, abs_dest, dst))
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            "Invalid path! (%s expands to an illegal path)" % dst
        })
        return (output_objects, returnvalues.CLIENT_ERROR)
    if not check_write_access(abs_dest, parent_dir=True):
        logger.warning('%s called without write access: %s' %
                       (op_name, abs_dest))
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            'cannot move to "%s": inside a read-only location!' % relative_dest
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    for pattern in src_list:
        unfiltered_match = glob.glob(base_dir + pattern)
        match = []
        for server_path in unfiltered_match:
            # IMPORTANT: path must be expanded to abs for proper chrooting
            abs_path = os.path.abspath(server_path)
            if not valid_user_path(configuration, abs_path, base_dir, True):
                logger.warning('%s tried to %s restricted path %s ! (%s)' %
                               (client_id, op_name, abs_path, pattern))
                continue
            match.append(abs_path)

        # Now actually treat list of allowed matchings and notify if no
        # (allowed) match

        if not match:
            output_objects.append({
                'object_type':
                'error_text',
                'text':
                '%s: no such file or directory! %s' % (op_name, pattern)
            })
            status = returnvalues.CLIENT_ERROR

        for abs_path in match:
            relative_path = abs_path.replace(base_dir, '')
            if verbose(flags):
                output_objects.append({
                    'object_type': 'file',
                    'name': relative_path
                })

            # Generally refuse handling symlinks including root vgrid shares
            if os.path.islink(abs_path):
                output_objects.append({
                    'object_type':
                    'warning',
                    'text':
                    """You're not allowed to
move entire special folders like %s shared folders!""" %
                    configuration.site_vgrid_label
                })
                status = returnvalues.CLIENT_ERROR
                continue
            # Additionally refuse operations on inherited subvgrid share roots
            elif in_vgrid_share(configuration, abs_path) == relative_path:
                output_objects.append({
                    'object_type':
                    'warning',
                    'text':
                    """You're not allowed to
move entire %s shared folders!""" % configuration.site_vgrid_label
                })
                status = returnvalues.CLIENT_ERROR
                continue
            elif os.path.realpath(abs_path) == os.path.realpath(base_dir):
                logger.error("%s: refusing move home dir: %s" %
                             (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'warning',
                    'text':
                    "You're not allowed to move your entire home directory!"
                })
                status = returnvalues.CLIENT_ERROR
                continue

            if not check_write_access(abs_path):
                logger.warning('%s called without write access: %s' %
                               (op_name, abs_path))
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    'cannot move "%s": inside a read-only location!' % pattern
                })
                status = returnvalues.CLIENT_ERROR
                continue

            # If destination is a directory the src should be moved in there
            # Move with existing directory as target replaces the directory!

            abs_target = abs_dest
            if os.path.isdir(abs_target):
                if os.path.samefile(abs_target, abs_path):
                    output_objects.append({
                        'object_type':
                        'warning',
                        'text':
                        "Cannot move '%s' to a subdirectory of itself!" %
                        relative_path
                    })
                    status = returnvalues.CLIENT_ERROR
                    continue
                abs_target = os.path.join(abs_target,
                                          os.path.basename(abs_path))

            try:
                gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'],
                          'moved', [relative_path, relative_dest])
                shutil.move(abs_path, abs_target)
                logger.info('%s %s %s done' % (op_name, abs_path, abs_target))
            except Exception as exc:
                if not isinstance(exc, GDPIOLogError):
                    gdp_iolog(configuration,
                              client_id,
                              environ['REMOTE_ADDR'],
                              'moved', [relative_path, relative_dest],
                              failed=True,
                              details=exc)
                output_objects.append({
                    'object_type':
                    'error_text',
                    'text':
                    "%s: '%s': %s" % (op_name, relative_path, exc)
                })
                logger.error("%s: failed on '%s': %s" %
                             (op_name, relative_path, exc))

                status = returnvalues.SYSTEM_ERROR
                continue

    return (output_objects, status)
Exemple #6
0
def main(client_id, user_arguments_dict, environ=None):
    """Main function used by front end"""

    if environ is None:
        environ = os.environ

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id)
    client_dir = client_id_dir(client_id)
    defaults = signature()[1]
    (validate_status, accepted) = validate_input_and_cert(
        user_arguments_dict,
        defaults,
        output_objects,
        client_id,
        configuration,
        allow_rejects=False,
        # NOTE: src and dst can use wildcards here
        typecheck_overrides={'src': valid_path_pattern,
                             'dst': valid_path_pattern},
    )
    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    flags = ''.join(accepted['flags'])
    src_list = accepted['src']
    dst = accepted['dst'][-1]
    iosessionid = accepted['iosessionid'][-1]
    share_id = accepted['share_id'][-1]
    freeze_id = accepted['freeze_id'][-1]

    if not safe_handler(configuration, 'post', op_name, client_id,
                        get_csrf_limit(configuration), accepted):
        output_objects.append(
            {'object_type': 'error_text', 'text': '''Only accepting
CSRF-filtered POST requests to prevent unintended updates'''
             })
        return (output_objects, returnvalues.CLIENT_ERROR)

    # Please note that base_dir must end in slash to avoid access to other
    # user dirs when own name is a prefix of another user name

    base_dir = os.path.abspath(os.path.join(configuration.user_home,
                                            client_dir)) + os.sep

    # Special handling if used from a job (no client_id but iosessionid)
    if not client_id and iosessionid:
        base_dir = os.path.realpath(configuration.webserver_home
                                    + os.sep + iosessionid) + os.sep

    # Use selected base as source and destination dir by default
    src_base = dst_base = base_dir

    # Sharelink import if share_id is given - change to sharelink as src base
    if share_id:
        try:
            (share_mode, _) = extract_mode_id(configuration, share_id)
        except ValueError as err:
            logger.error('%s called with invalid share_id %s: %s' %
                         (op_name, share_id, err))
            output_objects.append(
                {'object_type': 'error_text', 'text':
                 'Invalid sharelink ID: %s' % share_id})
            return (output_objects, returnvalues.CLIENT_ERROR)
        # TODO: load and check sharelink pickle (currently requires client_id)
        if share_mode == 'write-only':
            logger.error('%s called import from write-only sharelink: %s'
                         % (op_name, accepted))
            output_objects.append(
                {'object_type': 'error_text', 'text':
                 'Sharelink %s is write-only!' % share_id})
            return (output_objects, returnvalues.CLIENT_ERROR)
        target_dir = os.path.join(share_mode, share_id)
        src_base = os.path.abspath(os.path.join(configuration.sharelink_home,
                                                target_dir)) + os.sep
        if os.path.isfile(os.path.realpath(src_base)):
            logger.error('%s called import on single file sharelink: %s'
                         % (op_name, share_id))
            output_objects.append(
                {'object_type': 'error_text', 'text': """Import is only
supported for directory sharelinks!"""})
            return (output_objects, returnvalues.CLIENT_ERROR)
        elif not os.path.isdir(src_base):
            logger.error('%s called import with non-existant sharelink: %s'
                         % (client_id, share_id))
            output_objects.append(
                {'object_type': 'error_text', 'text': 'No such sharelink: %s'
                 % share_id})
            return (output_objects, returnvalues.CLIENT_ERROR)

    # Archive import if freeze_id is given - change to archive as src base
    if freeze_id:
        if not is_frozen_archive(client_id, freeze_id, configuration):
            logger.error('%s called with invalid freeze_id: %s' %
                         (op_name, freeze_id))
            output_objects.append(
                {'object_type': 'error_text', 'text':
                 'Invalid archive ID: %s' % freeze_id})
            return (output_objects, returnvalues.CLIENT_ERROR)
        target_dir = os.path.join(client_dir, freeze_id)
        src_base = os.path.abspath(os.path.join(configuration.freeze_home,
                                                target_dir)) + os.sep
        if not os.path.isdir(src_base):
            logger.error('%s called import with non-existant archive: %s'
                         % (client_id, freeze_id))
            output_objects.append(
                {'object_type': 'error_text', 'text': 'No such archive: %s'
                 % freeze_id})
            return (output_objects, returnvalues.CLIENT_ERROR)

    status = returnvalues.OK

    abs_dest = dst_base + dst
    dst_list = glob.glob(abs_dest)
    if not dst_list:

        # New destination?

        if not glob.glob(os.path.dirname(abs_dest)):
            logger.error('%s called with illegal dst: %s'
                         % (op_name, dst))
            output_objects.append(
                {'object_type': 'error_text', 'text':
                 'Illegal dst path provided!'})
            return (output_objects, returnvalues.CLIENT_ERROR)
        else:
            dst_list = [abs_dest]

    # Use last match in case of multiple matches

    dest = dst_list[-1]
    if len(dst_list) > 1:
        output_objects.append(
            {'object_type': 'warning', 'text':
             'dst (%s) matches multiple targets - using last: %s'
             % (dst, dest)})

    # IMPORTANT: path must be expanded to abs for proper chrooting
    abs_dest = os.path.abspath(dest)

    # Don't use abs_path in output as it may expose underlying
    # fs layout.

    relative_dest = abs_dest.replace(dst_base, '')
    if not valid_user_path(configuration, abs_dest, dst_base, True):
        logger.warning('%s tried to %s restricted path %s ! (%s)'
                       % (client_id, op_name, abs_dest, dst))
        output_objects.append(
            {'object_type': 'error_text', 'text':
             "Invalid destination (%s expands to an illegal path)" % dst})
        return (output_objects, returnvalues.CLIENT_ERROR)
    # We must make sure target dir exists if called in import X mode
    if (share_id or freeze_id) and not makedirs_rec(abs_dest, configuration):
        logger.error('could not create import destination dir: %s' % abs_dest)
        output_objects.append(
            {'object_type': 'error_text', 'text':
             'cannot import to "%s" : file in the way?' % relative_dest})
        return (output_objects, returnvalues.SYSTEM_ERROR)
    if not check_write_access(abs_dest, parent_dir=True):
        logger.warning('%s called without write access: %s'
                       % (op_name, abs_dest))
        output_objects.append(
            {'object_type': 'error_text', 'text':
             'cannot copy to "%s": inside a read-only location!'
             % relative_dest})
        return (output_objects, returnvalues.CLIENT_ERROR)
    if share_id and not force(flags) and not check_empty_dir(abs_dest):
        logger.warning('%s called %s sharelink import with non-empty dst: %s'
                       % (op_name, share_id, abs_dest))
        output_objects.append(
            {'object_type': 'error_text', 'text':
             """Importing a sharelink like '%s' into the non-empty '%s' folder
will potentially overwrite existing files with the sharelink version. If you
really want that, please try import again and select the overwrite box to
confirm it. You may want to back up any important data from %s first, however.
""" % (share_id, relative_dest, relative_dest)})
        return (output_objects, returnvalues.CLIENT_ERROR)
    if freeze_id and not force(flags) and not check_empty_dir(abs_dest):
        logger.warning('%s called %s archive import with non-empty dst: %s'
                       % (op_name, freeze_id, abs_dest))
        output_objects.append(
            {'object_type': 'error_text', 'text':
             """Importing an archive like '%s' into the non-empty '%s' folder
will potentially overwrite existing files with the archive version. If you
really want that, please try import again and select the overwrite box to
confirm it. You may want to back up any important data from %s first, however.
""" % (freeze_id, relative_dest, relative_dest)})
        return (output_objects, returnvalues.CLIENT_ERROR)

    for pattern in src_list:
        unfiltered_match = glob.glob(src_base + pattern)
        match = []
        for server_path in unfiltered_match:
            # IMPORTANT: path must be expanded to abs for proper chrooting
            abs_path = os.path.abspath(server_path)
            if not valid_user_path(configuration, abs_path, src_base, True):
                logger.warning('%s tried to %s restricted path %s ! (%s)'
                               % (client_id, op_name, abs_path, pattern))
                continue
            match.append(abs_path)

        # Now actually treat list of allowed matchings and notify if no
        # (allowed) match

        if not match:
            output_objects.append({'object_type': 'file_not_found',
                                   'name': pattern})
            status = returnvalues.FILE_NOT_FOUND

        for abs_path in match:
            relative_path = abs_path.replace(src_base, '')
            if verbose(flags):
                output_objects.append(
                    {'object_type': 'file', 'name': relative_path})

            # Prevent vgrid share copy which would create read-only dot dirs

            # Generally refuse handling symlinks including root vgrid shares
            if os.path.islink(abs_path):
                output_objects.append(
                    {'object_type': 'warning', 'text': """You're not allowed to
copy entire special folders like %s shared folders!"""
                     % configuration.site_vgrid_label})
                status = returnvalues.CLIENT_ERROR
                continue
            elif os.path.realpath(abs_path) == os.path.realpath(base_dir):
                logger.error("%s: refusing copy home dir: %s" % (op_name,
                                                                 abs_path))
                output_objects.append(
                    {'object_type': 'warning', 'text':
                     "You're not allowed to copy your entire home directory!"
                     })
                status = returnvalues.CLIENT_ERROR
                continue

            # src must be a file unless recursive is specified

            if not recursive(flags) and os.path.isdir(abs_path):
                logger.warning('skipping directory source %s' % abs_path)
                output_objects.append(
                    {'object_type': 'warning', 'text':
                     'skipping directory src %s!' % relative_path})
                continue

            # If destination is a directory the src should be copied there

            abs_target = abs_dest
            if os.path.isdir(abs_target):
                abs_target = os.path.join(abs_target,
                                          os.path.basename(abs_path))

            if os.path.abspath(abs_path) == os.path.abspath(abs_target):
                logger.warning('%s tried to %s %s to itself! (%s)'
                               % (client_id, op_name, abs_path, pattern))
                output_objects.append(
                    {'object_type': 'warning', 'text':
                     "Cannot copy '%s' to self!" % relative_path})
                status = returnvalues.CLIENT_ERROR
                continue
            if os.path.isdir(abs_path) and \
                    abs_target.startswith(abs_path + os.sep):
                logger.warning('%s tried to %s %s to itself! (%s)'
                               % (client_id, op_name, abs_path, pattern))
                output_objects.append(
                    {'object_type': 'warning', 'text':
                     "Cannot copy '%s' to (sub) self!" % relative_path})
                status = returnvalues.CLIENT_ERROR
                continue

            try:
                gdp_iolog(configuration,
                          client_id,
                          environ['REMOTE_ADDR'],
                          'copied',
                          [relative_path,
                           relative_dest
                           + "/" + os.path.basename(relative_path)])
                if os.path.isdir(abs_path):
                    shutil.copytree(abs_path, abs_target)
                else:
                    shutil.copy(abs_path, abs_target)
                logger.info('%s %s %s done' % (op_name, abs_path, abs_target))
            except Exception as exc:
                if not isinstance(exc, GDPIOLogError):
                    gdp_iolog(configuration,
                              client_id,
                              environ['REMOTE_ADDR'],
                              'copied',
                              [relative_path,
                               relative_dest
                               + "/" + os.path.basename(relative_path)],
                              failed=True,
                              details=exc)
                output_objects.append(
                    {'object_type': 'error_text',
                     'text': "%s: failed on '%s' to '%s'"
                     % (op_name, relative_path, relative_dest)})
                logger.error("%s: failed on '%s': %s" % (op_name,
                                                         relative_path, exc))
                status = returnvalues.SYSTEM_ERROR

    return (output_objects, status)