# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
#
import configparser
import locale
import os
import re
import stat
import syslog
import tempfile
from collections import namedtuple
class WebConfigParsingError(Exception):
def __init__(self, message):
self.message = message
class WebConfigMissing(Exception):
def __init__(self, message):
self.message = message
SECHEAD = 'asection'
def load(path, case_sensitive=False, ignore_bad_encoding=False):
config = configparser.ConfigParser(allow_no_value=True,
interpolation=None,
strict=False)
if case_sensitive:
config.optionxform = str
if ignore_bad_encoding:
with open(path, 'rb') as f:
raw = f.read().decode(locale.getpreferredencoding(), 'replace')
else:
with open(path, 'r', encoding='utf-8') as f:
raw = f.read()
config.read_string(f'[{SECHEAD}]\n' + raw, source=path)
return dict(config.items(section=SECHEAD))
_QUOTES = "'", '"'
def _strip_escape_quotes_of_config_value(val: str) -> str:
"""
Strips single or double quote char only if the quote present from both sides.
"""
if val.startswith(_QUOTES) and val.endswith(_QUOTES):
return val[1:-1]
return val
def load_fast(path, delimiter="=", strip_quotes=False):
data = {}
with open(path, "r", encoding="utf-8", errors="surrogateescape") as f:
for line in f.readlines():
parts = line.split(delimiter, 1)
try:
key, value = parts
except ValueError:
# Skip broken lines
continue
value = value.strip()
value = (
_strip_escape_quotes_of_config_value(value)
if strip_quotes
else value
)
data[key.strip()] = value
return data
cache = {}
def load_once(path, ignore_errors=False):
"""
Read ini file once (cached) and return its content as dict
"""
try:
res = cache[path]
except KeyError:
try:
res = cache[path] = load(path)
except (IOError, configparser.Error):
if not ignore_errors:
raise
res = cache[path] = {}
return res
_SAFE_KEY_RE = re.compile(r'^[A-Za-z0-9_.\\-]+$')
def _validate_conf_kv(key, value):
"""Reject keys/values that would inject config directives (F-11)."""
if not _SAFE_KEY_RE.match(key):
raise ValueError(f'change_settings: invalid key {key!r}')
if '\n' in str(value) or '\r' in str(value):
raise ValueError(f'change_settings: newline in value for key {key!r}')
def change_settings(settings_dict, path, tmp_path=None):
for k, v in settings_dict.items():
_validate_conf_kv(k, v)
used_keys = []
# Stage the rewrite in a temp file in the same directory as the target so
# the final os.replace() is atomic. With no caller-supplied path, use a
# random unique name (tempfile.mkstemp -> O_EXCL|O_CREAT, 0600): an
# unpredictable name defeats a symlink pre-plant (TOCTOU) and, unlike a
# fixed "path.tmp", a stale leftover from a prior crash never blocks future
# updates. A caller-supplied tmp_path is honoured but still created
# exclusively and without following symlinks.
if tmp_path:
fd = os.open(tmp_path,
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
else:
fd, tmp_path = tempfile.mkstemp(
dir=os.path.dirname(path) or '.',
prefix=os.path.basename(path) + '.', suffix='.tmp')
# The fdopen context manager owns fd and closes it on every exit path, so a
# failure in the mode-preserve block below cannot leak the descriptor.
try:
with os.fdopen(fd, 'w', encoding='utf-8') as fout, \
open(path, 'r', encoding='utf-8') as fin:
# Keep the rewritten config's permissions: configs like
# /etc/sysconfig/* are 0644 root:root, not 0600.
try:
orig = os.stat(path)
os.fchmod(fout.fileno(), stat.S_IMODE(orig.st_mode))
try:
os.fchown(fout.fileno(), orig.st_uid, orig.st_gid)
except PermissionError:
pass
except FileNotFoundError:
pass
for line in fin:
stripped_line = line.strip()
if stripped_line and not stripped_line.startswith('#'):
key, _ = stripped_line.split('=', 1)
key = key.strip()
if key in settings_dict:
fout.write(f'{key}={settings_dict[key]}\n')
used_keys.append(key)
continue
fout.write(line)
for key in settings_dict:
if key not in used_keys:
fout.write(f'{key}={settings_dict[key]}\n')
os.replace(tmp_path, path)
except BaseException:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
_NGINX_TOKENS_RE = re.compile(
r"""
(
# Comments
(:? \# .* $ )
# Single-, double-quoted strings and bare strings without whitespaces
| (:? "[^"\n]*?" )
| (:? '[^'\n]*?' )
| (:? [^"';\s\{\}]+ )
# Structural characters
| ;
| \{
| \}
| \n
)
""",
re.IGNORECASE | re.MULTILINE | re.VERBOSE,
)
def _ngx_tokenize(data):
tokens = (
match.group(0)
for match in _NGINX_TOKENS_RE.finditer(data)
if match and match.group(0)
)
# Explicitly ignore comments
return (tok for tok in tokens if not tok.startswith('#'))
def _ngx_take_until(it, val):
for tok in it:
if tok in val:
return
yield tok
def _ngx_take_until_block_end(it):
lvl = 1
for t in it:
if t == "{":
lvl += 1
elif t == "}":
lvl -= 1
if lvl < 1:
return
yield t
def _ngx_scan_block_info(block_tokens, need_fields):
"""Scan a block for required fields, skips nested blocks"""
info = {}
for tok in block_tokens:
# We need to skip until the end of inner block if it occurs
if tok == "{":
for _ in _ngx_take_until_block_end(block_tokens):
pass
# Now gather the value, the last occurrence is in priority
if tok in need_fields:
value_tokens = _ngx_take_until(block_tokens, ";\n")
info[tok] = list(value_tokens)
return info
def nginx_conf_loose_parser(data):
"""
Parse content of NGINX configuration in a manner tolerant to minor mistakes
and extract relevant fields from all `server` directives.
Relevant fields are:
- `server_name`
- `root` - returned as `document_root`
- `ssl` - if `listen` field contains "ssl" word
Doesn't handle interpolated values (ex. `${val}`) outside of quoted strings
"""
tokens = _ngx_tokenize(data)
for tok in tokens:
if tok != "server":
continue
# Nothing seems to be allowed between "server" directive and
# the opening of his block, so we just discard everything
# until first block opening seen
for _ in _ngx_take_until(tokens, "{"):
pass
# Limit further scan by the inside of block
block_tokens = _ngx_take_until_block_end(tokens)
# By using only `block_tokens` we ensure all blocks are properly delimited
info = _ngx_scan_block_info(block_tokens, ("server_name", "root", "listen"))
try:
server_name = info["server_name"]
root = info["root"]
except KeyError:
continue
if not server_name and not root:
continue
yield {
"server_name": _strip_escape_quotes_of_config_value(server_name[0]),
"document_root": _strip_escape_quotes_of_config_value(root[0]),
"ssl": "ssl" in info.get("listen", []),
}
def nginx_conf_parser(conf_file):
"""Parse NGINX config file, see `nginx_conf_loose_parser` for more details"""
if not os.path.isfile(conf_file):
raise WebConfigMissing(f'File does not exists {conf_file}')
dirty_data = read_unicode_file_with_decode_fallback(conf_file)
return list(nginx_conf_loose_parser(dirty_data))
def apache_conf_parser(conf_file):
if not os.path.isfile(conf_file):
raise WebConfigMissing(f'File does not exists {conf_file}')
conf_data = []
data_all = read_unicode_file_with_decode_fallback(conf_file).splitlines()
data = [i for i in data_all if re.search('^((?!#).)*$', i)]
ID = 0
enable = False
result = {}
vhost = []
while len(data) > 0:
out = data.pop(0)
if "