HEX
Server: nginx/1.22.0
System: Linux iZuf6jdxbygmf6cco977lcZ 5.10.84-10.4.al8.x86_64 #1 SMP Tue Apr 12 12:31:07 CST 2022 x86_64
User: root (0)
PHP: 7.4.29
Disabled: passthru,exec,system,chroot,chgrp,chown,shell_exec,proc_open,proc_get_status,ini_alter,ini_restore,dl,readlink,symlink,popepassthru,stream_socket_server,fsocket,popen
Upload Files
File: //usr/local/aegis/PythonLoaderTemp/third_party/crossplane/parser.py
# -*- coding: utf-8 -*-
import time
import glob
import logging
import os
from os import path

from .lexer import lex
from .analyzer import analyze, enter_block_ctx
from .errors import NgxParserDirectiveError

# map of external / third-party directives to a parse function
EXTERNAL_PARSERS = {}


def split_all(file_path):
    allparts = []
    while True:
        parts = path.split(file_path)
        if parts[0] == file_path:  # sentinel for absolute paths
            allparts.insert(0, parts[0])
            break
        elif parts[1] == file_path:  # sentinel for relative paths
            allparts.insert(0, parts[1])
            break
        else:
            file_path = parts[0]
            allparts.insert(0, parts[1])
    return allparts


def get_real_file_path(proc_root, filename):
    if proc_root != '/':
        filename = get_file_path_in_container(proc_root, filename)
    return filename


def get_file_path_in_container(proc_root, file_path):
    container_file_path = proc_root
    if proc_root in file_path:
        _, file_path = file_path.split(proc_root, 1)
    parts = split_all(file_path.lstrip('/'))
    for part in parts:
        container_file_path = path.join(container_file_path, part)
        while True:
            if not path.islink(container_file_path):
                break
            link_path = os.readlink(container_file_path)
            if path.isabs(link_path):
                container_file_path = path.join(proc_root, link_path.lstrip('/'))
            else:
                # used for /proc/29788/root/usr/bin/redis-server -> redis-check-rdb
                container_file_path = path.join(path.dirname(container_file_path), link_path)
    return path.normpath(container_file_path)


# TODO: raise special errors for invalid "if" args
def _prepare_if_args(stmt):
    """Removes parentheses from an "if" directive's arguments"""
    args = stmt['args']
    if args and args[0].startswith('(') and args[-1].endswith(')'):
        args[0] = args[0][1:].lstrip()
        args[-1] = args[-1][:-1].rstrip()
        start = int(not args[0])
        end = len(args) - int(not args[-1])
        args[:] = args[start:end]


def parse(filename, onerror=None, catch_errors=True, ignore=(), single=False,
        comments=False, strict=False, combine=False, check_ctx=True,
        check_args=True, procroot='/', includes_limit=None, interval=0,
          size_limit=0, logger_name=None):
    """
    Parses an nginx config file and returns a nested dict payload

    :param filename: string contianing the name of the config file to parse
    :param onerror: function that determines what's saved in "callback"
    :param catch_errors: bool; if False, parse stops after first error
    :param ignore: list or tuple of directives to exclude from the payload
    :param combine: bool; if True, use includes to create a single config obj
    :param single: bool; if True, including from other files doesn't happen
    :param comments: bool; if True, including comments to json payload
    :param strict: bool; if True, unrecognized directives raise errors
    :param check_ctx: bool; if True, runs context analysis on directives
    :param check_args: bool; if True, runs arg count analysis on directives

    :param procroot: using on container
    :param includes_limit: limit includes combine
    :param interval: interval on parsing includes
    :param size_limit: include file size limitation
    :param logger_name: logger name

    :returns: a payload that describes the parsed nginx config
    """
    logger = logging.getLogger(logger_name)

    config_dir = os.path.dirname(filename)

    payload = {
        'status': 'ok',
        'errors': [],
        'config': [],
    }

    config_file_size = os.path.getsize(filename)
    if size_limit != 0 and config_file_size > size_limit:
        logger.warning('ignore nginx conf size {} over {}. {}'.format(config_file_size, size_limit, filename))
        return payload

    # start with the main nginx config file/context
    includes = [(filename, ())]  # stores (filename, config context) tuples
    included = {filename: 0} # stores {filename: array index} map

    def _handle_error(parsing, e):
        """Adds representaions of an error to the payload"""
        file = parsing['file']
        error = str(e)
        line = getattr(e, 'lineno', None)

        parsing_error = {'error': error, 'line': line}
        payload_error = {'file': file, 'error': error, 'line': line}
        if onerror is not None:
            payload_error['callback'] = onerror(e)

        parsing['status'] = 'failed'
        parsing['errors'].append(parsing_error)

        payload['status'] = 'failed'
        payload['errors'].append(payload_error)

    def _parse(parsing, tokens, ctx=(), consume=False):
        """Recursively parses nginx config contexts"""
        fname = parsing['file']
        parsed = []

        # parse recursively by pulling from a flat stream of tokens
        for token, lineno, quoted in tokens:
            comments_in_args = []

            # we are parsing a block, so break if it's closing
            if token == '}' and not quoted:
                break

            # if we are consuming, then just continue until end of context
            if consume:
                # if we find a block inside this context, consume it too
                if token == '{' and not quoted:
                    _parse(parsing, tokens, consume=True)
                continue

            # the first token should always(?) be an nginx directive
            directive = token

            if combine:
                stmt = {
                    'file': fname,
                    'directive': directive,
                    'line': lineno,
                    'args': []
                }
            else:
                stmt = {
                    'directive': directive,
                    'line': lineno,
                    'args': []
                }

            # if token is comment
            if directive.startswith('#') and not quoted:
                if comments:
                    stmt['directive'] = '#'
                    stmt['comment'] = token[1:]
                    parsed.append(stmt)
                continue

            # TODO: add external parser checking and handling

            # parse arguments by reading tokens
            args = stmt['args']
            token, __, quoted = next(tokens)  # disregard line numbers of args
            while token not in ('{', ';', '}') or quoted:
                if token.startswith('#') and not quoted:
                    comments_in_args.append(token[1:])
                else:
                    stmt['args'].append(token)

                token, __, quoted = next(tokens)

            # consume the directive if it is ignored and move on
            if stmt['directive'] in ignore:
                # if this directive was a block consume it too
                if token == '{' and not quoted:
                    _parse(parsing, tokens, consume=True)
                continue

            # prepare arguments
            if stmt['directive'] == 'if':
                _prepare_if_args(stmt)

            try:
                # raise errors if this statement is invalid
                analyze(
                    fname=fname, stmt=stmt, term=token, ctx=ctx, strict=strict,
                    check_ctx=check_ctx, check_args=check_args
                )
            except NgxParserDirectiveError as e:
                if catch_errors:
                    _handle_error(parsing, e)

                    # if it was a block but shouldn't have been then consume
                    if e.strerror.endswith(' is not terminated by ";"'):
                        if token != '}' and not quoted:
                            _parse(parsing, tokens, consume=True)
                        else:
                            break

                    # keep on parsin'
                    continue
                else:
                    raise e

            # add "includes" to the payload if this is an include statement
            if not single and stmt['directive'] == 'include':
                pattern = args[0]
                if not os.path.isabs(args[0]):
                    pattern = os.path.join(config_dir, args[0])
                pattern = get_real_file_path(procroot, pattern)

                stmt['includes'] = []

                # get names of all included files
                if glob.has_magic(pattern):
                    fnames = glob.glob(pattern)
                    # fnames.sort()
                    if includes_limit is not None and len(fnames) > includes_limit:
                        logger.warning('too many includes({}), includes limit {}'.format(len(fnames), includes_limit))
                        fnames = fnames[:includes_limit]
                else:
                    try:
                        # if the file pattern was explicit, nginx will check
                        # that the included file can be opened and read
                        open(str(pattern)).close()
                        fnames = [pattern]
                    except Exception as e:
                        fnames = []
                        e.lineno = stmt['line']
                        if catch_errors:
                            _handle_error(parsing, e)
                        else:
                            raise e

                for fname in fnames:
                    # the included set keeps files from being parsed twice
                    # TODO: handle files included from multiple contexts
                    if fname not in included:
                        included[fname] = len(includes)
                        includes.append((fname, ctx))
                    index = included[fname]
                    stmt['includes'].append(index)

            # if this statement terminated with '{' then it is a block
            if token == '{' and not quoted:
                inner = enter_block_ctx(stmt, ctx)  # get context for block
                stmt['block'] = _parse(parsing, tokens, ctx=inner)

            parsed.append(stmt)

            # add all comments found inside args after stmt is added
            for comment in comments_in_args:
                comment_stmt = {
                    'directive': '#',
                    'line': stmt['line'],
                    'args': [],
                    'comment': comment
                }
                parsed.append(comment_stmt)

        return parsed

    # the includes list grows as "include" directives are found in _parse
    for fname, ctx in includes:
        fname = get_real_file_path(procroot, fname)

        parsing = {
            'file': fname,
            'status': 'ok',
            'errors': [],
            'parsed': []
        }

        if not os.path.exists(fname):
            continue
        config_file_size = os.path.getsize(fname)
        if size_limit != 0 and config_file_size > size_limit:
            logger.warning('ignore nginx conf size {} over {}. {}'.format(config_file_size, size_limit, fname))
        else:
            tokens = lex(fname)
            try:
                parsing['parsed'] = _parse(parsing, tokens, ctx=ctx)
                time.sleep(interval)
            except Exception as e:
                _handle_error(parsing, e)

        payload['config'].append(parsing)

    if combine:
        return _combine_parsed_configs(payload)
    else:
        return payload


def _combine_parsed_configs(old_payload):
    """
    Combines config files into one by using include directives.

    :param old_payload: payload that's normally returned by parse()
    :return: the new combined payload
    """
    old_configs = old_payload['config']

    def _perform_includes(block):
        for stmt in block:
            if 'block' in stmt:
                stmt['block'] = list(_perform_includes(stmt['block']))
            if 'includes' in stmt:
                for index in stmt['includes']:
                    config = old_configs[index]['parsed']
                    for stmt in _perform_includes(config):
                        yield stmt
            else:
                yield stmt  # do not yield include stmt itself

    combined_config = {
        'file': old_configs[0]['file'],
        'status': 'ok',
        'errors': [],
        'parsed': []
    }

    for config in old_configs:
        combined_config['errors'] += config.get('errors', [])
        if config.get('status', 'ok') == 'failed':
            combined_config['status'] = 'failed'

    first_config = old_configs[0]['parsed']
    combined_config['parsed'] += _perform_includes(first_config)

    combined_payload = {
        'status': old_payload.get('status', 'ok'),
        'errors': old_payload.get('errors', []),
        'config': [combined_config]
    }
    return combined_payload


def register_external_parser(parser, directives):
    """
    :param parser: parser function
    :param directives: list of directive strings
    :return:
    """
    for directive in directives:
        EXTERNAL_PARSERS[directive] = parser