pax_global_header00006660000000000000000000000064136133210510014505gustar00rootroot0000000000000052 comment=4598e85cabd3a2ad79ce3725c7165ee6fe4e2750 postfix-mta-sts-resolver-0.7.5/000077500000000000000000000000001361332105100164375ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/.dockerignore000066400000000000000000000000741361332105100211140ustar00rootroot00000000000000venv pkg_venv *.egg-info contrib config_examples Dockerfile postfix-mta-sts-resolver-0.7.5/.github/000077500000000000000000000000001361332105100177775ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/.github/ISSUE_TEMPLATE/000077500000000000000000000000001361332105100221625ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012261361332105100246550ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: Snawoot --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. 2. 3. 4. **Expected behavior** A clear and concise description of what you expected to happen. **Output listings** If applicable, add output listings to help explain your problem. **Environment (please complete the following information):** - OS: [e.g. GNU/Linux] - Distro: [e.g. Debian] - Distro version: [e.g. 9] - Python version: [e.g. 3.6] **Additional context** Add any other context about the problem here. postfix-mta-sts-resolver-0.7.5/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011411361332105100257040ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: Snawoot --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. postfix-mta-sts-resolver-0.7.5/.gitignore000066400000000000000000000023511361332105100204300ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ pkg_venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ man/*.? *.db *.pem *.der *.cer *.crt *.key postfix-mta-sts-resolver-0.7.5/.pylintrc000066400000000000000000000422501361332105100203070ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=print-statement, parameter-unpacking, unpacking-in-except, old-raise-syntax, backtick, long-suffix, old-ne-operator, old-octal-literal, import-star-module-level, non-ascii-bytes-literal, raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, apply-builtin, basestring-builtin, buffer-builtin, cmp-builtin, coerce-builtin, execfile-builtin, file-builtin, long-builtin, raw_input-builtin, reduce-builtin, standarderror-builtin, unicode-builtin, xrange-builtin, coerce-method, delslice-method, getslice-method, setslice-method, no-absolute-import, old-division, dict-iter-method, dict-view-method, next-method-called, metaclass-assignment, indexing-exception, raising-string, reload-builtin, oct-method, hex-method, nonzero-method, cmp-method, input-builtin, round-builtin, intern-builtin, unichr-builtin, map-builtin-not-iterating, zip-builtin-not-iterating, range-builtin-not-iterating, filter-builtin-not-iterating, using-cmp-argument, eq-without-hash, div-method, idiv-method, rdiv-method, exception-message-attribute, invalid-str-codec, sys-max-int, bad-python3-import, deprecated-string-function, deprecated-str-translate-call, deprecated-itertools-function, deprecated-types-field, next-method-defined, dict-items-not-iterating, dict-keys-not-iterating, dict-values-not-iterating, deprecated-operator-function, deprecated-urllib-function, xreadlines-attribute, deprecated-sys-function, exception-escape, comprehension-escape, missing-docstring, import-error, broad-except, no-else-return # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma, dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [STRING] # This flag controls whether the implicit-str-concat-in-sequence should # generate a warning on implicit string concatenation in sequences defined over # several lines. check-str-concat-over-line-jumps=no [LOGGING] # Format style used to check logging format string. `old` means using % # formatting, while `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, ex, Run, _ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. #variable-rgx= [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package.. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [DESIGN] # Maximum number of arguments for function / method. max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". overgeneral-exceptions=BaseException, Exception postfix-mta-sts-resolver-0.7.5/.travis.yml000066400000000000000000000011141361332105100205450ustar00rootroot00000000000000language: python dist: xenial matrix: include: - python: 3.8 env: TOXENV=lint - python: 3.8 env: TOXENV=cover - python: 3.5 env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: 3.7 env: TOXENV=py37 - python: 3.8 env: TOXENV=py38 - python: 3.5 env: TOXENV=py35-uvloop - python: 3.6 env: TOXENV=py36-uvloop - python: 3.7 env: TOXENV=py37-uvloop - python: 3.8 env: TOXENV=py38-uvloop install: - "sudo -H env PYTHON=\"$(command -v python)\" tests/install.debian.sh" script: - tox postfix-mta-sts-resolver-0.7.5/Dockerfile000066400000000000000000000015351361332105100204350ustar00rootroot00000000000000FROM docker.io/python:3.8-alpine LABEL maintainer="Vladislav Yarmak " ARG UID=18721 ARG USER=mta-sts ARG GID=18721 RUN true \ && addgroup --gid "$GID" "$USER" \ && adduser \ --disabled-password \ --gecos "" \ --home "/build" \ --ingroup "$USER" \ --no-create-home \ --uid "$UID" \ "$USER" \ && true COPY . /build WORKDIR /build RUN true \ && apk add --no-cache --virtual .build-deps alpine-sdk libffi-dev \ && apk add --no-cache libffi \ && pip3 install --no-cache-dir .[sqlite,redis,uvloop] \ && mkdir /var/lib/mta-sts \ && chown -R "$USER:$USER" /build /var/lib/mta-sts \ && apk del .build-deps \ && true COPY docker-config.yml /etc/mta-sts-daemon.yml USER $USER VOLUME [ "/var/lib/mta-sts" ] EXPOSE 8461/tcp ENTRYPOINT [ "mta-sts-daemon" ] postfix-mta-sts-resolver-0.7.5/LICENSE000066400000000000000000000020611361332105100174430ustar00rootroot00000000000000MIT License Copyright (c) 2018 Vladislav Yarmak Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. postfix-mta-sts-resolver-0.7.5/Makefile000066400000000000000000000027741361332105100201110ustar00rootroot00000000000000PYTHON = python3 RM = rm PKG_NAME = postfix_mta_sts_resolver ARCH_NAME = postfix-mta-sts-resolver MANPAGES = $(patsubst %.adoc,%,$(wildcard man/*.adoc)) PRJ_DIR = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) VENV ?= $(PRJ_DIR)venv PKGVENV ?= $(PRJ_DIR)pkg_venv install: $(VENV) setup.py $(VENV)/bin/python -m pip install -U .[sqlite,redis,dev] $(VENV): $(PYTHON) -m venv $(VENV) $(VENV)/bin/python -m pip install -U wheel uninstall: $(VENV) $(VENV)/bin/python -m pip uninstall -y $(PKG_NAME) man/%: asciidoctor --backend=manpage $@.adoc doc: $(MANPAGES) clean: $(RM) -rf $(VENV) $(PKGVENV) dist/ build/ $(PKG_NAME).egg-info/ man/*.? $(PKGVENV): $(PYTHON) -m venv $(PKGVENV) $(PKGVENV)/bin/python -m pip install -U setuptools wheel twine pkg: $(PKGVENV) $(PKGVENV)/bin/python setup.py sdist bdist_wheel $(PKG_NAME).egg-info/PKG-INFO: $(PKGVENV) $(PKGVENV)/bin/python setup.py egg_info version: $(PKG_NAME).egg-info/PKG-INFO @echo Evaluating pagkage version... $(eval PKG_VERSION := $(if $(PKG_VERSION),$(PKG_VERSION),$(shell grep -Po '(?<=^Version: ).*' $<))) @echo Version = $(PKG_VERSION) upload: pkg version $(PKGVENV)/bin/python -m twine upload dist/$(PKG_NAME)-$(PKG_VERSION)* testupload: pkg version $(PKGVENV)/bin/python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/$(PKG_NAME)-$(PKG_VERSION)* archive: version git archive --prefix=$(ARCH_NAME)-$(PKG_VERSION)/ -o ../$(ARCH_NAME)-$(PKG_VERSION).tar.gz v$(PKG_VERSION) .PHONY: install clean uninstall pkg version archive postfix-mta-sts-resolver-0.7.5/PULL_REQUEST_TEMPLATE.md000066400000000000000000000003731361332105100222430ustar00rootroot00000000000000**Purpose of proposed changes** Please give us an idea about why these changes should be applied. **Essential steps taken** If possible, please highlight most essential parts of your solution in order to speed up our understanding of changes made. postfix-mta-sts-resolver-0.7.5/README.md000066400000000000000000000254351361332105100177270ustar00rootroot00000000000000postfix-mta-sts-resolver ======================== [![Build Status](https://travis-ci.org/Snawoot/postfix-mta-sts-resolver.svg?branch=master)](https://travis-ci.org/Snawoot/postfix-mta-sts-resolver) [![Coverage](https://img.shields.io/badge/coverage-97%25-4dc71f.svg)](https://travis-ci.org/Snawoot/postfix-mta-sts-resolver) [![PyPI - Downloads](https://img.shields.io/pypi/dm/postfix-mta-sts-resolver.svg?color=4dc71f&label=PyPI%20downloads)](https://pypistats.org/packages/postfix-mta-sts-resolver) [![PyPI](https://img.shields.io/pypi/v/postfix-mta-sts-resolver.svg)](https://pypi.org/project/postfix-mta-sts-resolver/) [![PyPI - Status](https://img.shields.io/pypi/status/postfix-mta-sts-resolver.svg)](https://pypi.org/project/postfix-mta-sts-resolver/) [![PyPI - License](https://img.shields.io/pypi/l/postfix-mta-sts-resolver.svg?color=4dc71f)](https://pypi.org/project/postfix-mta-sts-resolver/) Daemon which provides TLS client policy for Postfix via socketmap, according to domain MTA-STS policy. Current support of RFC8461 is limited - daemon lacks some minor features: * Proactive policy fetch * Fetch error reporting * Fetch ratelimit (but actual fetch rate partially restricted with `cache_grace` config option). Server has configurable cache backend which allows to store cached STS policies in memory (`internal`), file (`sqlite`) or in Redis database (`redis`). ## Requirements * Postfix 2.10 and later * Python 3.5.3+ (see ["Systems without Python 3.5+"](#systems-without-python-35) below if you haven't one, or use Docker installation method) * aiodns * aiohttp * aiosqlite * aioredis * PyYAML * (optional) uvloop All dependency packages installed automatically if this package is installed via pip. ## Installation ### Method 1. System-wide install from PyPI (recommended for humans) Run: ```bash sudo python3 -m pip install postfix-mta-sts-resolver[redis,sqlite] ``` If you don't need `redis` or `sqlite` support, you may omit one of them in square brackets. If you don't need any of them and you plan to use internal cache without persistence, you should also omit square brackets. Package scripts shall be available in standard executable locations upon completion. #### pip user install All pip invocations can be run with `--user` option of `pip` installer. In this case superuser privileges are not required and package(s) are getting installed into user home directory. Usually, script executables will appear in `~/.local/bin`. ### Method 2. System-wide install from project source Run in project directory: ```bash sudo python3 -m pip install .[redis,sqlite] ``` If you don't need `redis` or `sqlite` support, you may omit one of them in square brackets. If you don't need any of them and you plan to use internal cache without persistence, you should also omit square brackets. Package scripts shall be available in standard executable locations upon completion. ### Method 3. Install into virtualenv See ["Building virtualenv"](#building-virtualenv) ### Method 4. Docker Run ```bash docker volume create mta-sts-cache docker run -d \ --security-opt no-new-privileges \ -v mta-sts-cache:/var/lib/mta-sts \ -p 127.0.0.1:8461:8461 \ --restart unless-stopped \ --name postfix-mta-sts-resolver \ yarmak/postfix-mta-sts-resolver ``` Daemon will be up and running, listening on local interface on port 8461. Default configuration baked into docker image uses SQLite for cache stored in persistent docker volume. You may override this configuration with your own config file by mapping it into container with option `-v my_config.yml:/etc/mta-sta-daemon.yml`. ### Common installation notes See also [contrib/README.md](https://github.com/Snawoot/postfix-mta-sts-resolver/tree/master/contrib/README.md) for RHEL/OEL/Centos and FreeBSD notes. See [contrib/](https://github.com/Snawoot/postfix-mta-sts-resolver/tree/master/contrib) for example of systemd unit file suitable to run daemon under systemd control. ## Running This package provides two executables available after installation in respective locations. ### mta-sts-query `mta-sts-query` is a command line tool which fetches and outputs domain MTA-STS policies. Intended to be used for debug purposes. Synopsis: ``` $ mta-sts-query --help usage: mta-sts-query [-h] [-v {debug,info,warn,error,fatal}] domain [known_version] positional arguments: domain domain to fetch MTA-STS policy from known_version latest known version (default: None) optional arguments: -h, --help show this help message and exit -v {debug,info,warn,error,fatal}, --verbosity {debug,info,warn,error,fatal} logging verbosity (default: warn) ``` ### mta-sts-daemon `mta-sts-daemon` is a daemon which provides external [TLS policy for Postfix SMTP client](http://www.postfix.org/TLS_README.html#client_tls_policy) via [socketmap interface](http://www.postfix.org/socketmap_table.5.html). You may find useful systemd unit file to run daemon in [contrib/](https://github.com/Snawoot/postfix-mta-sts-resolver/tree/master/contrib). Synopsis: ``` $ mta-sts-daemon --help usage: mta-sts-daemon [-h] [-v {debug,info,warn,error,fatal}] [-c FILE] [-l FILE] [--disable-uvloop] optional arguments: -h, --help show this help message and exit -v {debug,info,warn,error,fatal}, --verbosity {debug,info,warn,error,fatal} logging verbosity (default: info) -c FILE, --config FILE config file location (default: /etc/mta-sts- daemon.yml) -l FILE, --logfile FILE log file location (default: None) --disable-uvloop do not use uvloop even if it is available (default: False) ``` #### Seamless restart/upgrade/reload and load balancing By default mta-sts-daemon allows its multiple instances to share same port (on Linux/FreeBSD/Windows). Therefore, restart or upgrade of daemon can be performed seamlessly. Set of unit files for systemd in [contrib/](contrib/) directory implements "reload" by mean of running backup instance when main instance is getting restarted. Also on Linux and FreeBSD, load distribited across all processes (with SO\_REUSEPORT and SO\_REUSEPORT\_LB respectively). ## MTA-STS Daemon configuration See [configuration man page](https://github.com/Snawoot/postfix-mta-sts-resolver/blob/master/man/mta-sts-daemon.yml.5.adoc) and [config\_examples/](https://github.com/Snawoot/postfix-mta-sts-resolver/tree/master/config_examples) directory. Default config location is: `/etc/mta-sts-daemon.yml`, but it can be overriden with command line option `-c FILE`. All options is self-explanatory, only exception is `strict_testing` option. If set to `true`, STS policy will be enforced even if domain announces `testing` MTA-STS mode. Useful for premature incorporation of MTA-STS against domains hesistating to go `enforce`. Please use with caution. ## Postfix configuration SMTP client of your Postfix instance must be able to validate peer certificates. In order to achieve that, you have to ensure [`smtp_tls_CAfile`](http://www.postfix.org/postconf.5.html#smtp_tls_CAfile) or [`smtp_tls_CApath`](http://www.postfix.org/postconf.5.html#smtp_tls_CApath) points to system CA bundle. Otherwise you'll get `Unverified TLS connection` even for peers with valid certificate, and delivery failures for MTA-STS-enabled destinations. Also note: even enabled [`tls_append_default_CA`](http://www.postfix.org/postconf.5.html#tls_append_default_CA) will not work alone if both `smtp_tls_CAfile` and `smtp_tls_CApath` are empty. Once certificate validation is enabled and your Postfix log shows "Trusted TLS connection ... " for destinations with valid certificates signed by public CA, you may enable MTA-STS by adding following line to `main.cf`: ``` smtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix ``` If your configuration already has some TLS policy maps, just add MTA-STS socketmap to list of configured maps accordingly to [`smtp_tls_policy_maps`](http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps) syntax. TLS policy tables are searched in the specified order until a match is found, so you may have table with local overrides of TLS policy prior to MTA-STS socketmap. This may be useful for skipping network lookup for well-known destinations or relaxing security for broken destinations, announcing MTA-STS support. Reload Postfix after reconfiguration. ## Operability check Assuming default MTA-STA daemon configuration. Following command: ```bash /usr/sbin/postmap -q dismail.de socketmap:inet:127.0.0.1:8461:postfix ``` should return something like: ``` secure match=mx1.dismail.de ``` Postfix log should show `Verified TLS connection established to ...` instead of `Untrusted ...` or `Trusted TLS connection established to ...` when mail is getting sent to MTA-STS-enabled domain. ## Special cases of deployment ### Systems without Python 3.5+ Some people may find convenient to install latest python from source into `/opt` directory. This way you can have separate python installation not interferring with system packages by any means. Download latest python source from [python.org](https://www.python.org/), unpack and run in unpacked source directory: ```bash ./configure --prefix=/opt --enable-optimizations && make -j $[ $(nproc) + 1 ] && make test && sudo make install ``` Python binaries will be available in `/opt/bin`, including `pip3`. You may install `postfix-mta-sts-resolver` using `/opt/bin/pip3` without interference with any system packages: ```bash sudo /opt/bin/pip3 install postfix-mta-sts-resolver[sqlite,redis] ``` Executable files of `postfix-mta-sts-resolver` will be available in `/opt/bin/mta-sts-query` and `/opt/bin/mta-sts-daemon` ### Building virtualenv Run `make` in project directory in order to build virtualenv. As result of it, new directory `venv` shall appear. `venv` contains interpreter and all required dependencies, i.e. encloses package with depencencies in separate environment. It is possible to specify alternative path where virtualenv directory shall be placed. Specify VENV variable for `make` command. Example: ```bash make VENV=~/postfix-mta-sts-resolver ``` Such virtual environment can be moved to another machine of similar type (as far python interpreter is compatible with new environment). If virtualenv is placed into same location on new machine, application can be runned this way: ```bash venv/bin/mta-sts-daemon ``` Otherwise, some hacks required. First option - explicitly call virtualenv interpreter: ```bash venv/bin/python venv/bin/mta-sts-daemon ``` Second option - specify new path in shebang of scripts installed in virtualenv. It is recommended to build virtualenv at same location which app shall occupy on target system. ## Credits Inspired by [this forum thread](http://postfix.1071664.n5.nabble.com/MTA-STS-when-td95086.html). postfix-mta-sts-resolver-0.7.5/SECURITY.md000066400000000000000000000021501361332105100202260ustar00rootroot00000000000000# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | > 0.2.9 | :heavy_check_mark: | | < 0.2.9 | :x: | Since new versions are backwards compatible with all older versions only few recent versions are supported. In case of security problem with old unsupported version recommended solution is an upgrade. Latest version will receive security fixes with release of new fixed version. Older versions will receive security fixes as set of applicable patches. Security fixes for latest version is a first priority. ## Reporting a Vulnerability Please report vulnerabilities via email to vladislav (at) vm-0.com. If possible, please use PGP encryption with [my public key](https://keybase.io/yarmak/pgp_keys.asc). Only vulnerabilities directly related to this software are accepted. Vulnerabilities related to MTA-STS standard itself will be rejected and you'll have to report them to standard authors by yourself. You may expect first preliminary response on vulnerability report within hours and fix within one or few days, depending on issue complexity. postfix-mta-sts-resolver-0.7.5/config_examples/000077500000000000000000000000001361332105100216025ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/config_examples/mta-sts-daemon.yml.internal000066400000000000000000000003471361332105100267750ustar00rootroot00000000000000host: 127.0.0.1 port: 8461 reuse_port: true shutdown_timeout: 20 cache: type: internal options: cache_size: 10000 default_zone: strict_testing: false timeout: 4 zones: myzone: strict_testing: false timeout: 4 postfix-mta-sts-resolver-0.7.5/config_examples/mta-sts-daemon.yml.redis000066400000000000000000000004321361332105100262620ustar00rootroot00000000000000host: 127.0.0.1 port: 8461 reuse_port: true shutdown_timeout: 20 cache: type: redis options: address: "redis://127.0.0.1/0?timeout=5" minsize: 5 maxsize: 25 default_zone: strict_testing: false timeout: 4 zones: myzone: strict_testing: false timeout: 4 postfix-mta-sts-resolver-0.7.5/config_examples/mta-sts-daemon.yml.sqlite000066400000000000000000000003711361332105100264570ustar00rootroot00000000000000host: 127.0.0.1 port: 8461 reuse_port: true shutdown_timeout: 20 cache: type: sqlite options: filename: "/var/lib/mta-sts/cache.db" default_zone: strict_testing: false timeout: 4 zones: myzone: strict_testing: false timeout: 4 postfix-mta-sts-resolver-0.7.5/config_examples/mta-sts-daemon.yml.sqlite_unixsock000066400000000000000000000003661361332105100304060ustar00rootroot00000000000000path: "/var/run/mta-sts.sock" mode: 0666 shutdown_timeout: 20 cache: type: sqlite options: filename: "/var/lib/mta-sts/cache.db" default_zone: strict_testing: false timeout: 4 zones: myzone: strict_testing: false timeout: 4 postfix-mta-sts-resolver-0.7.5/contrib/000077500000000000000000000000001361332105100200775ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/contrib/README.md000066400000000000000000000014541361332105100213620ustar00rootroot00000000000000# Deployment ## RHEL/CentOS/OEL The default Python version in RHEL7 (and clones) is too old, you need at least 3.5 to run postfix-mta-sts. However, Python 3.6 is availale in the Software collections For other distributions please edit the Systemd Unit file accordingly ### Create a User (and Group) to run MTA-STS ```bash useradd -c "Daemon for MTA-STS policy checks" mta-sts -s /sbin/nologin ``` ### Systemd Unit file Place the provided files to /etc/systemd/system and reload the system daemon ```bash systemctl daemon-reload ``` To enable MTA-STS on system startup run ```bash systemctl enable postfix-mta-sts.service ``` ## FreeBSD rc.d file Place the provided mta-sts-daemon file to /usr/local/etc/rc.d To enable MTA-STS on system startup add `mta_sts_daemon_enable="YES"` to your /etc/rc.conf postfix-mta-sts-resolver-0.7.5/contrib/mta-sts-daemon000066400000000000000000000012161361332105100226530ustar00rootroot00000000000000#!/bin/sh # # # PROVIDE: mta-sts-daemon # REQUIRE: LOGIN # KEYWORD: shutdown # # Add the following line to /etc/rc.conf to enable `mta-sts-daemon': # # mta_sts_daemon_enable="YES" . /etc/rc.subr name="mta_sts_daemon" rcvar=`set_rcvar` load_rc_config $name mta_sts_daemon_enable=${mta_sts_daemon_enable:=NO} config="/usr/local/etc/postfix/mta-sts-daemon.yml" log_file="/var/log/mta-sts.log" log_verbosity="info" mta_sts_daemon_user="mta-sts" procname="/usr/local/bin/python3.6" command="/usr/local/bin/mta-sts-daemon -v ${log_verbosity} -c ${config} -l ${log_file}" start_cmd="/usr/sbin/daemon -u $mta_sts_daemon_user $command" run_rc_command "$1" postfix-mta-sts-resolver-0.7.5/contrib/postfix-mta-sts-daemon@.service000066400000000000000000000007041361332105100261050ustar00rootroot00000000000000[Unit] Description=Postfix MTA STS daemon instance After=syslog.target network.target [Service] Type=notify User=mta-sts Group=mta-sts # This is the ExecStart path for RHEL7 using python 36 from the Software collections. # You may use a different python interpreter on other distributions ExecStart=/opt/rh/rh-python36/root/bin/mta-sts-daemon Restart=always KillMode=process TimeoutStartSec=10 TimeoutStopSec=30 [Install] WantedBy=multi-user.target postfix-mta-sts-resolver-0.7.5/contrib/postfix-mta-sts.service000066400000000000000000000007471361332105100245530ustar00rootroot00000000000000[Unit] Description=Postfix MTA STS daemon After=syslog.target network.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/bin/systemctl start postfix-mta-sts-daemon@main.service ExecReload=/bin/systemctl start postfix-mta-sts-daemon@backup.service ; \ /bin/systemctl restart postfix-mta-sts-daemon@main.service ; \ /bin/systemctl stop postfix-mta-sts-daemon@backup.service ExecStop=/bin/systemctl stop postfix-mta-sts-daemon@main.service [Install] WantedBy=multi-user.target postfix-mta-sts-resolver-0.7.5/docker-config.yml000066400000000000000000000002751361332105100217000ustar00rootroot00000000000000host: 0.0.0.0 port: 8461 reuse_port: true shutdown_timeout: 20 cache: type: sqlite options: filename: "/var/lib/mta-sts/cache.db" default_zone: strict_testing: false timeout: 4 postfix-mta-sts-resolver-0.7.5/man/000077500000000000000000000000001361332105100172125ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/man/mta-sts-daemon.1.adoc000066400000000000000000000026741361332105100230430ustar00rootroot00000000000000= mta-sts-daemon(1) :doctype: manpage :manmanual: mta-sts-daemon :mansource: postfix-mta-sts-resolver == Name mta-sts-daemon - provide MTA-STS policy to Postfix as policy map == Synopsis *mta-sts-daemon* [_OPTION_]... == Description This daemon opens a socket where Postfix can query and retrieve the MTA-STS policy for a domain. The configuration file is described in *mta-sts-daemon.yml*(5). MTA-STS, specified in RFC 8461 [0], is a security standard for email servers. When a site configures MTA-STS, other mail servers can require the successful authentication of that site when forwarding mail there. == Options *-h, --help*:: show a help message and exit *-v, --verbosity* _VERBOSITY_:: set log verbosity level: _debug_, _info_ (default), _warn_, _error_, or _fatal_. *-c, --config* _FILE_:: config file location (default: _/etc/mta-sts-daemon.yml_) *-l, --logfile* _FILE_:: log file location (default: _none_) *--disable-uvloop*:: do not use uvloop even if it is available (default: enabled if available) == Examples Configure Postfix in _/etc/postfix/main.cf_: smtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix smtp_tls_CApath = /etc/ssl/certs/ Reload Postfix. Then verify it works: */usr/sbin/postmap -q dismail.de socketmap:inet:127.0.0.1:8461:postfix* == See also *mta-sts-query*(1), *mta-sts-daemon.yml*(5) == Notes 0.:: *SMTP MTA Strict Transport Security (MTA-STS)*: https://tools.ietf.org/html/rfc8461 postfix-mta-sts-resolver-0.7.5/man/mta-sts-daemon.yml.5.adoc000066400000000000000000000051301361332105100236350ustar00rootroot00000000000000= mta-sts-daemon.yml(5) :doctype: manpage :manmanual: mta-sts-daemon.yml :mansource: postfix-mta-sts-resolver == Name mta-sts-daemon.yml - configuration file for mta-sts-daemon == Description This configuration file configures the listening socket, caching behaviour, and manipulation of MTA-STS mode. == Syntax The file is in YAML syntax with the following elements: *host*: (_str_) daemon bind address *port*: (_int_) daemon bind port *path*: (_str_) daemon UNIX socket bind address (path). If specified, *host* and *port* are ignored and UNIX socket is bound instead of TCP. *mode*: (_int_) file mode for daemon UNIX socket. If not specified default filemode is used. This option has effect only when UNIX socket is used. If file mode specified in octal form (most common case), it has to be prepended with leading zero. Example: 0666 *reuse_port*: (_bool_) allow multiple instances to share same port (available on Unix, Windows) *cache_grace*: (_float_) age of cache entries in seconds which do not require policy refresh and update. Default: 60 *shutdown_timeout*: (_float_) time limit granted to existing client sessions for finishing when server stops. Default: 20 *cache*:: * *type*: (_str_: _internal_|_sqlite_|_redis_) cache backend type * *options*: ** Options for _internal_ type: *** *cache_size*: (_int_) number of cache entries to store in memory ** Options for _sqlite_ type: *** *filename*: (_str_) path to database file *** *threads*: (_int_) number of threads in pool for SQLite connections *** *timeout*: (_float_) timeout in seconds for acquiring connection from pool or DB lock ** Options for _redis_ type: *** All parameters are passed to `aioredis.create_redis_pool` [0]. Check there for a parameter reference. *default_zone*:: * *strict_testing*: (_bool_) enforce policy for testing domains * *timeout*: (_int_) network operations timeout for resolver in that zone *zones*:: * *ZONENAME*: ** Same as options in _default_zone_ The timeout is used for the DNS and HTTP requests. MTA-STS "testing" mode can be interpreted as "strict" mode. This may be useful (though noncompliant) in the beginning of MTA-STS deployment, when many domains operate under "testing" mode. == Example host: 127.0.0.1 port: 8461 reuse_port: true shutdown_timeout: 20 cache: type: internal options: cache_size: 10000 default_zone: strict_testing: false timeout: 4 zones: myzone: strict_testing: false timeout: 4 == See also *mta-sts-daemon*(1), *mta-sts-query*(1) == Notes 0.:: https://aioredis.readthedocs.io/en/latest/api_reference.html#aioredis.create_redis_pool postfix-mta-sts-resolver-0.7.5/man/mta-sts-query.1.adoc000066400000000000000000000021611361332105100227340ustar00rootroot00000000000000= mta-sts-query(1) :doctype: manpage :manmanual: mta-sts-query :mansource: postfix-mta-sts-resolver == Name mta-sts-query - retrieve MTA-STS policy of a domain == Synopsis *mta-sts-query* [_OPTION_]... _DOMAIN_ [_KNOWN_VERSION_] == Description Retrieve the MTA-STS policy of a domain and display the result on standard output. This diagnostic utility is also useful without Postfix. MTA-STS, specified in RFC 8461 [0], is a security standard for email servers. When a site configures MTA-STS, other mail servers can require the successful authentication of that site when forwarding mail there. == Options *-h, --help*:: show a help message and exit *-v, --verbosity* _VERBOSITY_:: set verbosity level: _debug_, _info_, _warn_ (default), _error_, or _fatal_. *DOMAIN*:: domain for which to retrieve the MTA-STS policy *KNOWN_VERSION*:: latest known version (default: None) == Examples Retrieve MTA-STS policy for dismail.de: *mta-sts-query dismail.de* == See also *mta-sts-daemon*(1), *mta-sts-daemon.yml*(5) == Notes 0.:: *SMTP MTA Strict Transport Security (MTA-STS)*: https://tools.ietf.org/html/rfc8461 postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/000077500000000000000000000000001361332105100236065ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/__init__.py000066400000000000000000000000001361332105100257050ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/__main__.py000077500000000000000000000014531361332105100257060ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import asyncio from .resolver import STSResolver def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("domain", help="domain to fetch MTA-STS policy from") parser.add_argument("known_version", nargs="?", default=None, help="latest known version") return parser.parse_args() def main(): # pragma: no cover args = parse_args() loop = asyncio.get_event_loop() resolver = STSResolver(loop=loop) result = loop.run_until_complete(resolver.resolve(args.domain, args.known_version)) print(result) if __name__ == '__main__': # pragma: no cover main() postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/asdnotify.py000066400000000000000000000040611361332105100261610ustar00rootroot00000000000000import os import socket import asyncio MAX_QLEN = 128 class AsyncSystemdNotifier: def __init__(self): env_var = os.getenv('NOTIFY_SOCKET') self._addr = ('\0' + env_var[1:] if env_var is not None and env_var.startswith('@') else env_var) self._sock = None self._started = False self._loop = None self._queue = asyncio.Queue(MAX_QLEN) self._monitor = False @property def started(self): return self._started def _drain(self): while not self._queue.empty(): msg = self._queue.get_nowait() self._queue.task_done() try: self._send(msg) except BlockingIOError: # pragma: no cover self._monitor = True self._loop.add_writer(self._sock.fileno(), self._drain) break except OSError: pass else: if self._monitor: self._monitor = False self._loop.remove_writer(self._sock.fileno()) def _send(self, data): return self._sock.sendto(data, socket.MSG_NOSIGNAL, self._addr) async def start(self): if self._addr is None: return False self._loop = asyncio.get_event_loop() try: self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self._sock.setblocking(0) self._started = True except OSError: return False return True async def notify(self, status): if self._started: await self._queue.put(status) self._drain() async def stop(self): if self._started: self._started = False await self._queue.join() if self._monitor: self._loop.remove_writer(self._sock.fileno()) self._sock.close() async def __aenter__(self): await self.start() return self async def __aexit__(self, exc_type, exc, traceback): await self.stop() postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/base_cache.py000066400000000000000000000007611361332105100262210ustar00rootroot00000000000000import collections from abc import ABC, abstractmethod CacheEntry = collections.namedtuple('CacheEntry', ('ts', 'pol_id', 'pol_body')) class BaseCache(ABC): @abstractmethod async def setup(self): """ Abstract method """ @abstractmethod async def get(self, key): """ Abstract method """ @abstractmethod async def set(self, key, value): """ Abstract method """ @abstractmethod async def teardown(self): """ Abstract method """ postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/constants.py000066400000000000000000000001201361332105100261650ustar00rootroot00000000000000HARD_RESP_LIMIT = 64 * 1024 CHUNK = 4096 QUEUE_LIMIT = 128 REQUEST_LIMIT = 1024 postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/daemon.py000066400000000000000000000073221361332105100254270ustar00rootroot00000000000000#!/usr/bin/env python3 import os import argparse import asyncio import logging import signal from functools import partial from .asdnotify import AsyncSystemdNotifier from . import utils from . import defaults from .responder import STSSocketmapResponder def parse_args(): def check_loglevel(arg): try: return utils.LogLevel[arg] except (IndexError, KeyError): raise argparse.ArgumentTypeError("%s is not valid loglevel" % (repr(arg),)) parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("-v", "--verbosity", help="logging verbosity", type=check_loglevel, choices=utils.LogLevel, default=utils.LogLevel.info) parser.add_argument("-c", "--config", help="config file location", metavar="FILE", default=defaults.CONFIG_LOCATION) parser.add_argument("-l", "--logfile", help="log file location", metavar="FILE") parser.add_argument("--disable-uvloop", help="do not use uvloop even if it is available", action="store_true") return parser.parse_args() def exit_handler(exit_event, signum, frame): # pragma: no cover pylint: disable=unused-argument logger = logging.getLogger('MAIN') if exit_event.is_set(): logger.warning("Got second exit signal! Terminating hard.") os._exit(1) # pylint: disable=protected-access else: logger.warning("Got first exit signal! Terminating gracefully.") exit_event.set() async def heartbeat(): """ Hacky coroutine which keeps event loop spinning with some interval even if no events are coming. This is required to handle Futures and Events state change when no events are occuring.""" while True: await asyncio.sleep(.5) async def amain(cfg, loop): # pragma: no cover logger = logging.getLogger("MAIN") # Construct request handler instance responder = STSSocketmapResponder(cfg, loop) await responder.start() logger.info("Server started.") exit_event = asyncio.Event() beat = asyncio.ensure_future(heartbeat()) sig_handler = partial(exit_handler, exit_event) signal.signal(signal.SIGTERM, sig_handler) signal.signal(signal.SIGINT, sig_handler) async with AsyncSystemdNotifier() as notifier: await notifier.notify(b"READY=1") await exit_event.wait() logger.debug("Eventloop interrupted. Shutting down server...") await notifier.notify(b"STOPPING=1") beat.cancel() await responder.stop() def main(): # pragma: no cover # Parse command line arguments and setup basic logging args = parse_args() with utils.AsyncLoggingHandler(args.logfile) as log_handler: logger = utils.setup_logger('MAIN', args.verbosity, log_handler) utils.setup_logger('STS', args.verbosity, log_handler) logger.info("MTA-STS daemon starting...") # Read config and populate with defaults cfg = utils.load_config(args.config) # Construct event loop logger.info("Starting eventloop...") if not args.disable_uvloop: if utils.enable_uvloop(): logger.info("uvloop enabled.") else: logger.info("uvloop is not available. " "Falling back to built-in event loop.") evloop = asyncio.get_event_loop() logger.info("Eventloop started.") evloop.run_until_complete(amain(cfg, evloop)) evloop.close() logger.info("Server finished its work.") postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/defaults.py000066400000000000000000000005571361332105100257760ustar00rootroot00000000000000from multiprocessing import cpu_count HOST = "127.0.0.1" PORT = 8461 REUSE_PORT = True TIMEOUT = 4 SHUTDOWN_TIMEOUT = 20 STRICT_TESTING = False CONFIG_LOCATION = "/etc/mta-sts-daemon.yml" CACHE_BACKEND = "internal" INTERNAL_CACHE_SIZE = 10000 SQLITE_THREADS = cpu_count() SQLITE_TIMEOUT = 5 REDIS_TIMEOUT = 5 CACHE_GRACE = 60 USER_AGENT = "postfix-mta-sts-resolver" postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/internal_cache.py000066400000000000000000000013471361332105100271240ustar00rootroot00000000000000import collections from .base_cache import BaseCache class InternalLRUCache(BaseCache): def __init__(self, cache_size=10000): self._cache_size = cache_size self._cache = collections.OrderedDict() async def setup(self): pass async def teardown(self): pass async def get(self, key): try: value = self._cache.pop(key) self._cache[key] = value return value except KeyError: return None async def set(self, key, value): try: self._cache.pop(key) except KeyError: if len(self._cache) >= self._cache_size: self._cache.popitem(last=False) self._cache[key] = value postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/netstring.py000066400000000000000000000076751361332105100262140ustar00rootroot00000000000000import ssl COLON = b':' COMMA = b',' ZERO = b'0' ZERO_ORD = ord(ZERO) class NetstringException(Exception): pass class WantRead(NetstringException): pass class InappropriateParserState(NetstringException): pass class ParseError(NetstringException): pass class IncompleteNetstring(ParseError): pass class TooLong(ParseError): pass class BadLength(ParseError): pass class BadTerminator(ParseError): pass class SingleNetstringFetcher: def __init__(self, incoming, maxlen=-1): self._incoming = incoming self._maxlen = maxlen self._len_known = False self._len = None self._done = False self._length_bytes = b'' def done(self): return self._done def pending(self): return self._len is not None def read(self, nbytes=65536): # pylint: disable=too-many-branches if not self._len_known: # reading length while True: symbol = self._incoming.read(1) if not symbol: raise WantRead() if symbol == COLON: if self._len is None: raise BadLength("No netstring length digits seen.") self._len_known = True break if not symbol.isdigit(): raise BadLength("Non-digit symbol in netstring length.") val = ord(symbol) - ZERO_ORD self._len = val if self._len is None else self._len * 10 + val if self._maxlen != -1 and self._len > self._maxlen: raise TooLong("Netstring length is over limit.") # reading data if self._len: buf = self._incoming.read(min(nbytes, self._len)) if not buf: raise WantRead() self._len -= len(buf) return buf else: if not self._done: symbol = self._incoming.read(1) if not symbol: raise WantRead() if symbol == COMMA: self._done = True else: raise BadTerminator("Bad netstring terminator.") return b'' class StreamReader: """ Async Netstring protocol decoder with interface alike to ssl.SSLObject BIO interface. next_string() method returns SingleNetstringFetcher class which fetches parts of netstring. SingleNestringFetcher.read() returns b'' in case of string end or raises WantRead exception when StreamReader needs to be filled with additional data. Parsing errors signalized with exceptions subclassing ParseError""" def __init__(self, maxlen=-1): """ Creates StreamReader instance. Params: maxlen - maximal allowed netstring length. """ self._maxlen = maxlen self._incoming = ssl.MemoryBIO() self._fetcher = None def pending(self): return self._fetcher is not None and self._fetcher.pending() def feed(self, data): self._incoming.write(data) def next_string(self): if self._fetcher is not None and not self._fetcher.done(): raise InappropriateParserState("next_string() invoked while " "previous fetcher is not exhausted") self._fetcher = SingleNetstringFetcher(self._incoming, self._maxlen) return self._fetcher def encode(data): return b'%d:%s,' % (len(data), data) def decode(data): reader = StreamReader() reader.feed(data) try: while True: res = [] string_reader = reader.next_string() while True: buf = string_reader.read() if not buf: break res.append(buf) yield b''.join(res) except WantRead: if reader.pending(): raise IncompleteNetstring("Input ends on unfinished string.") postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/redis_cache.py000066400000000000000000000035331361332105100264150ustar00rootroot00000000000000import json import uuid import aioredis from . import defaults from .base_cache import BaseCache, CacheEntry def pack_entry(entry): ts, pol_id, pol_body = entry # pylint: disable=invalid-name,unused-variable obj = (pol_id, pol_body) # add unique seed to entry in order to avoid set collisions # and use ZSET two-index table packed = uuid.uuid4().bytes + json.dumps(obj).encode('utf-8') return packed def unpack_entry(packed): bin_obj = packed[16:] obj = json.loads(bin_obj.decode('utf-8')) pol_id, pol_body = obj return CacheEntry(ts=0, pol_id=pol_id, pol_body=pol_body) class RedisCache(BaseCache): def __init__(self, **opts): self._opts = dict(opts) self._opts['timeout'] = self._opts.get('timeout', defaults.REDIS_TIMEOUT) self._opts['encoding'] = None self._pool = None async def setup(self): self._pool = await aioredis.create_redis_pool(**self._opts) async def get(self, key): assert self._pool is not None key = key.encode('utf-8') res = await self._pool.zrevrange(key, 0, 0, "WITHSCORES") if not res: return None packed, ts = res[0] # pylint: disable=invalid-name entry = unpack_entry(packed) return CacheEntry(ts=ts, pol_id=entry.pol_id, pol_body=entry.pol_body) async def set(self, key, value): assert self._pool is not None packed = pack_entry(value) ts = value.ts # pylint: disable=invalid-name key = key.encode('utf-8') # Write pipe = self._pool.pipeline() pipe.zadd(key, ts, packed) pipe.zremrangebyrank(key, 0, -2) await pipe.execute() async def teardown(self): assert self._pool is not None self._pool.close() await self._pool.wait_closed() postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/resolver.py000066400000000000000000000141761361332105100260320ustar00rootroot00000000000000import asyncio import enum from io import BytesIO import aiodns import aiodns.error import aiohttp from . import defaults from .utils import parse_mta_sts_record, parse_mta_sts_policy, is_plaintext, filter_text from .constants import HARD_RESP_LIMIT, CHUNK class BadSTSPolicy(Exception): pass class STSFetchResult(enum.Enum): NONE = 0 VALID = 1 FETCH_ERROR = 2 NOT_CHANGED = 3 _HEADERS = {"User-Agent": defaults.USER_AGENT} # pylint: disable=too-few-public-methods class STSResolver: def __init__(self, *, timeout=defaults.TIMEOUT, loop): self._loop = loop self._timeout = timeout self._resolver = aiodns.DNSResolver(timeout=timeout, loop=loop) self._http_timeout = aiohttp.ClientTimeout(total=timeout) self._proxy_info = aiohttp.helpers.proxies_from_env().get('https', None) if self._proxy_info is None: self._proxy = None self._proxy_auth = None else: self._proxy = self._proxy_info.proxy self._proxy_auth = self._proxy_info.proxy_auth # pylint: disable=too-many-locals,too-many-branches,too-many-return-statements async def resolve(self, domain, last_known_id=None): if domain.startswith('.'): return STSFetchResult.NONE, None # Cleanup domain name domain = domain.rstrip('.') # Construct name of corresponding MTA-STS DNS record for domain sts_txt_domain = '_mta-sts.' + domain # Try to fetch it try: txt_records = await asyncio.wait_for( self._resolver.query(sts_txt_domain, 'TXT'), timeout=self._timeout) except aiodns.error.DNSError as error: if error.args[0] == aiodns.error.ARES_ETIMEOUT: # pragma: no cover pylint: disable=no-else-return,no-member # This branch is not covered because of aiodns bug: # https://github.com/saghul/aiodns/pull/64 # It's hard to decide what to do in case of timeout # Probably it's better to threat this as fetch error # so caller probably shall report such cases. return STSFetchResult.FETCH_ERROR, None elif error.args[0] == aiodns.error.ARES_ENOTFOUND: # pylint: disable=no-else-return,no-member return STSFetchResult.NONE, None elif error.args[0] == aiodns.error.ARES_ENODATA: # pylint: disable=no-else-return,no-member return STSFetchResult.NONE, None else: # pragma: no cover return STSFetchResult.NONE, None except asyncio.TimeoutError: return STSFetchResult.FETCH_ERROR, None # workaround for floating return type of pycares txt_records = filter_text(rec.text for rec in txt_records) # RFC 8461 strictly defines version string as first field txt_records = [txt for txt in txt_records if txt.startswith('v=STSv1')] # Exactly one record should exist if len(txt_records) != 1: return STSFetchResult.NONE, None # Validate record mta_sts_record = parse_mta_sts_record(txt_records[0]) if (mta_sts_record.get('v', None) != 'STSv1' or 'id' not in mta_sts_record): return STSFetchResult.NONE, None # Obtain policy ID and return NOT_CHANGED if ID is equal to last known if mta_sts_record['id'] == last_known_id: return STSFetchResult.NOT_CHANGED, None # Construct corresponding URL of MTA-STS policy sts_policy_url = ('https://mta-sts.' + domain + '/.well-known/mta-sts.txt') # Fetch actual policy try: async with aiohttp.ClientSession(loop=self._loop, timeout=self._http_timeout) \ as session: async with session.get(sts_policy_url, allow_redirects=False, proxy=self._proxy, headers=_HEADERS, proxy_auth=self._proxy_auth) as resp: if resp.status != 200: raise BadSTSPolicy() if not is_plaintext(resp.headers.get('Content-Type', '')): raise BadSTSPolicy() if (int(resp.headers.get('Content-Length', '0')) > HARD_RESP_LIMIT): raise BadSTSPolicy() policy_file = BytesIO() while policy_file.tell() <= HARD_RESP_LIMIT: chunk = await resp.content.read(CHUNK) if not chunk: break policy_file.write(chunk) else: raise BadSTSPolicy() charset = (resp.charset if resp.charset is not None else 'ascii') policy_text = policy_file.getvalue().decode(charset) except Exception: return STSFetchResult.FETCH_ERROR, None # Parse policy pol = parse_mta_sts_policy(policy_text) # Validate policy if pol.get('version', None) != 'STSv1': return STSFetchResult.FETCH_ERROR, None try: max_age = int(pol.get('max_age', '-1')) pol['max_age'] = max_age except ValueError: return STSFetchResult.FETCH_ERROR, None if not 0 <= max_age <= 31557600: return STSFetchResult.FETCH_ERROR, None if 'mode' not in pol: return STSFetchResult.FETCH_ERROR, None # No MX check required for 'none' policy: if pol['mode'] == 'none': return STSFetchResult.VALID, (mta_sts_record['id'], pol) if pol['mode'] not in ('none', 'testing', 'enforce'): return STSFetchResult.FETCH_ERROR, None if not pol['mx']: return STSFetchResult.FETCH_ERROR, None # Policy is valid. Returning result. return STSFetchResult.VALID, (mta_sts_record['id'], pol) postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/responder.py000066400000000000000000000264051361332105100261700ustar00rootroot00000000000000import asyncio import logging import time import collections import sys import os import socket from functools import partial from .resolver import STSResolver, STSFetchResult from .constants import QUEUE_LIMIT, CHUNK, REQUEST_LIMIT from .utils import create_custom_socket, create_cache, filter_domain, is_ipaddr from .base_cache import CacheEntry from . import netstring ZoneEntry = collections.namedtuple('ZoneEntry', ('strict', 'resolver')) # pylint: disable=too-many-instance-attributes class STSSocketmapResponder: def __init__(self, cfg, loop): self._logger = logging.getLogger("STS") self._loop = loop if cfg.get('path') is not None: self._unix = True self._path = cfg['path'] self._sockmode = cfg.get('mode') else: self._unix = False self._host = cfg['host'] self._port = cfg['port'] self._reuse_port = cfg['reuse_port'] self._shutdown_timeout = cfg['shutdown_timeout'] self._grace = cfg['cache_grace'] # Construct configurations and resolvers for every socketmap name self._default_zone = ZoneEntry(cfg["default_zone"]["strict_testing"], STSResolver(loop=loop, timeout=cfg["default_zone"]["timeout"])) self._zones = dict((k, ZoneEntry(zone["strict_testing"], STSResolver(loop=loop, timeout=zone["timeout"]))) for k, zone in cfg["zones"].items()) # Construct cache self._cache = create_cache(cfg["cache"]["type"], cfg["cache"]["options"]) self._children = set() self._server = None async def start(self): def _spawn(reader, writer): def done_cb(task, fut): self._children.discard(task) task = self._loop.create_task(self.handler(reader, writer)) task.add_done_callback(partial(done_cb, task)) self._children.add(task) self._logger.debug("len(self._children) = %d", len(self._children)) await self._cache.setup() if self._unix: self._server = await asyncio.start_unix_server(_spawn, path=self._path) if self._sockmode is not None: os.chmod(self._path, self._sockmode) else: if self._reuse_port: # pragma: no cover if sys.platform in ('win32', 'cygwin'): opts = { 'host': self._host, 'port': self._port, 'reuse_address': True, } elif os.name == 'posix': if sys.platform.startswith('freebsd'): sockopts = [ (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), (socket.SOL_SOCKET, 0x10000, 1), # SO_REUSEPORT_LB ] sock = await create_custom_socket(self._host, self._port, options=sockopts) opts = { 'sock': sock, } else: opts = { 'host': self._host, 'port': self._port, 'reuse_address': True, 'reuse_port': True, } self._server = await asyncio.start_server(_spawn, **opts) async def stop(self): self._server.close() await self._server.wait_closed() while True: self._logger.warning("Awaiting %d client handlers to finish...", len(self._children)) remaining = asyncio.gather(*self._children, return_exceptions=True) self._children.clear() try: await asyncio.wait_for(remaining, self._shutdown_timeout) except asyncio.TimeoutError: self._logger.warning("Shutdown timeout expired. " "Remaining handlers terminated.") try: await remaining except asyncio.CancelledError: pass await asyncio.sleep(1) if not self._children: break await self._cache.teardown() async def sender(self, queue, writer): def cleanup_queue(): while not queue.empty(): task = queue.get_nowait() try: task.cancel() except Exception: # pragma: no cover pass try: while True: fut = await queue.get() # Check for shutdown if fut is None: return self._logger.debug("Got new future from queue") data = await fut self._logger.debug("Future await complete: data=%s", repr(data)) writer.write(data) self._logger.debug("Wrote: %s", repr(data)) await writer.drain() except asyncio.CancelledError: cleanup_queue() except Exception as exc: # pragma: no cover self._logger.exception("Exception in sender coro: %s", exc) cleanup_queue() finally: writer.close() # pylint: disable=too-many-locals,too-many-branches,too-many-statements async def process_request(self, raw_req): # Update local cache async def cache_set(domain, entry): try: await self._cache.set(domain, entry) except asyncio.CancelledError: # pragma: no cover pylint: disable=try-except-raise raise except Exception as exc: # pragma: no cover self._logger.exception("Cache set failed: %s", str(exc)) have_policy = True # Parse request and canonicalize domain req_zone, _, req_domain = raw_req.decode('ascii').partition(' ') domain = filter_domain(req_domain) # Skip lookups for parent domain policies # Skip lookups to non-domains if domain.startswith('.') or is_ipaddr(domain): return netstring.encode(b'NOTFOUND ') # Find appropriate zone config if req_zone in self._zones: zone_cfg = self._zones[req_zone] else: zone_cfg = self._default_zone # Lookup for cached policy try: cached = await self._cache.get(domain) except asyncio.CancelledError: # pragma: no cover pylint: disable=try-except-raise raise except Exception as exc: # pragma: no cover self._logger.exception("Cache get failed: %s", str(exc)) cached = None ts = time.time() # pylint: disable=invalid-name # Check if cached record exists and recent enough to omit # DNS lookup and cache update if cached is None or ts - cached.ts > self._grace: self._logger.debug("Lookup PERFORMED: domain = %s", domain) # Check if newer policy exists or # retrieve policy from scratch if there is no cached one latest_pol_id = None if cached is None else cached.pol_id status, policy = await zone_cfg.resolver.resolve(domain, latest_pol_id) if status is STSFetchResult.NOT_CHANGED: cached = CacheEntry(ts, cached.pol_id, cached.pol_body) await cache_set(domain, cached) elif status is STSFetchResult.VALID: pol_id, pol_body = policy cached = CacheEntry(ts, pol_id, pol_body) await cache_set(domain, cached) else: if cached is None: have_policy = False else: # Check if cached policy is expired if cached.pol_body['max_age'] + cached.ts < ts: have_policy = False else: self._logger.debug("Lookup skipped: domain = %s", domain) if have_policy: mode = cached.pol_body['mode'] # pylint: disable=no-else-return if mode == 'none' or (mode == 'testing' and not zone_cfg.strict): return netstring.encode(b'NOTFOUND ') else: assert cached.pol_body['mx'], "Empty MX list for restrictive policy!" mxlist = [mx.lstrip('*') for mx in set(cached.pol_body['mx'])] resp = "OK secure match=" + ":".join(mxlist) return netstring.encode(resp.encode('utf-8')) else: return netstring.encode(b'NOTFOUND ') async def handler(self, reader, writer): # Construct netstring parser stream_reader = netstring.StreamReader(REQUEST_LIMIT) # Construct queue for responses ordering queue = asyncio.Queue(QUEUE_LIMIT, loop=self._loop) # Create coroutine which awaits for steady responses and sends them sender = asyncio.ensure_future(self.sender(queue, writer), loop=self._loop) class EndOfStream(Exception): pass async def finalize(): try: await queue.put(None) except asyncio.CancelledError: # pragma: no cover sender.cancel() raise await sender try: while True: # Extract and parse request string_reader = stream_reader.next_string() request_parts = [] while True: try: buf = string_reader.read() except netstring.WantRead: part = await reader.read(CHUNK) if not part: raise EndOfStream() self._logger.debug("Read: %s", repr(part)) stream_reader.feed(part) else: if buf: request_parts.append(buf) else: req = b''.join(request_parts) self._logger.debug("Enq request: %s", repr(req)) fut = asyncio.ensure_future(self.process_request(req), loop=self._loop) await queue.put(fut) break except netstring.ParseError: self._logger.warning("Bad netstring message received") await finalize() except (EndOfStream, ConnectionError, TimeoutError): self._logger.debug("Client disconnected") await finalize() except OSError as exc: # pragma: no cover if exc.errno == 107: self._logger.debug("Client disconnected") await finalize() else: self._logger.exception("Unhandled exception: %s", exc) await finalize() except asyncio.CancelledError: sender.cancel() raise except Exception as exc: # pragma: no cover self._logger.exception("Unhandled exception: %s", exc) await finalize() postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/sqlite_cache.py000066400000000000000000000121451361332105100266070ustar00rootroot00000000000000# pylint: disable=invalid-name,protected-access import asyncio import sqlite3 import json import logging import aiosqlite from .defaults import SQLITE_THREADS, SQLITE_TIMEOUT from .base_cache import BaseCache, CacheEntry class SqliteConnPool: def __init__(self, threads, conn_args=(), conn_kwargs=None, init_queries=()): self._threads = threads self._conn_args = conn_args self._conn_kwargs = conn_kwargs if conn_kwargs is not None else {} self._init_queries = init_queries self._free_conns = asyncio.Queue() self._ready = False self._stopped = False async def _new_conn(self): db = await aiosqlite.connect(*self._conn_args, **self._conn_kwargs) try: async with db.cursor() as cur: for q in self._init_queries: await cur.execute(q) except: await db.close() raise return db async def prepare(self): for _ in range(self._threads): self._free_conns.put_nowait(await self._new_conn()) self._ready = True async def stop(self): self._ready = False self._stopped = True try: while True: db = self._free_conns.get_nowait() await db.close() except asyncio.QueueEmpty: pass def borrow(self, timeout=None): if not self._ready: raise RuntimeError("Pool not prepared!") class PoolBorrow: # pylint: disable=no-self-argument def __init__(s): s._conn = None # pylint: disable=no-self-argument async def __aenter__(s): s._conn = await asyncio.wait_for(self._free_conns.get(), timeout) return s._conn # pylint: disable=no-self-argument async def __aexit__(s, exc_type, exc, tb): if self._stopped: await s._conn.close() return if exc_type is not None: await s._conn.close() s._conn = await self._new_conn() self._free_conns.put_nowait(s._conn) return PoolBorrow() class SqliteCache(BaseCache): def __init__(self, filename, *, threads=SQLITE_THREADS, timeout=SQLITE_TIMEOUT): self._filename = filename self._threads = threads self._timeout = timeout sqlitelogger = logging.getLogger("aiosqlite") if not sqlitelogger.hasHandlers(): # pragma: no cover sqlitelogger.addHandler(logging.NullHandler()) self._pool = None async def setup(self): conn_init = [ "PRAGMA journal_mode=WAL", "PRAGMA synchronous=NORMAL", ] self._pool = SqliteConnPool(self._threads, conn_args=(self._filename,), conn_kwargs={ "timeout": self._timeout, }, init_queries=conn_init) await self._pool.prepare() queries = [ "create table if not exists sts_policy_cache " "(domain text, ts integer, pol_id text, pol_body text)", "create unique index if not exists sts_policy_domain on sts_policy_cache (domain)", "create index if not exists sts_policy_domain_ts on sts_policy_cache (domain, ts)", ] async with self._pool.borrow(self._timeout) as conn: async with conn.cursor() as cur: for q in queries: await cur.execute(q) await conn.commit() async def get(self, key): async with self._pool.borrow(self._timeout) as conn: async with conn.execute('select ts, pol_id, pol_body from ' 'sts_policy_cache where domain=?', (key,)) as cur: res = await cur.fetchone() if res is not None: ts, pol_id, pol_body = res ts = int(ts) pol_body = json.loads(pol_body) return CacheEntry(ts, pol_id, pol_body) else: return None async def set(self, key, value): ts, pol_id, pol_body = value pol_body = json.dumps(pol_body) async with self._pool.borrow(self._timeout) as conn: try: await conn.execute('insert into sts_policy_cache (domain, ts, ' 'pol_id, pol_body) values (?, ?, ?, ?)', (key, int(ts), pol_id, pol_body)) await conn.commit() except sqlite3.IntegrityError: await conn.execute('update sts_policy_cache set ts = ?, ' 'pol_id = ?, pol_body = ? where domain = ? ' 'and ts < ?', (int(ts), pol_id, pol_body, key, int(ts))) await conn.commit() async def teardown(self): await self._pool.stop() postfix-mta-sts-resolver-0.7.5/postfix_mta_sts_resolver/utils.py000066400000000000000000000142251361332105100253240ustar00rootroot00000000000000import enum import logging import logging.handlers import asyncio import socket import queue import yaml from . import defaults class LogLevel(enum.IntEnum): debug = logging.DEBUG info = logging.INFO warn = logging.WARN error = logging.ERROR fatal = logging.FATAL crit = logging.CRITICAL def __str__(self): return self.name class OverflowingQueue(queue.Queue): def put(self, item, block=True, timeout=None): try: return queue.Queue.put(self, item, block, timeout) except queue.Full: pass def put_nowait(self, item): return self.put(item, False) class AsyncLoggingHandler: def __init__(self, logfile=None, maxsize=1024): _queue = OverflowingQueue(maxsize) if logfile is None: _handler = logging.StreamHandler() else: _handler = logging.FileHandler(logfile) self._listener = logging.handlers.QueueListener(_queue, _handler) self._async_handler = logging.handlers.QueueHandler(_queue) _handler.setFormatter(logging.Formatter('%(asctime)s ' '%(levelname)-8s ' '%(name)s: %(message)s', '%Y-%m-%d %H:%M:%S')) def __enter__(self): self._listener.start() return self._async_handler def __exit__(self, exc_type, exc_value, traceback): self._listener.stop() def setup_logger(name, verbosity, handler): logger = logging.getLogger(name) logger.setLevel(verbosity) logger.addHandler(handler) return logger def enable_uvloop(): # pragma: no cover try: # pylint: disable=import-outside-toplevel import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: return False else: return True def populate_cfg_defaults(cfg): if not cfg: cfg = {} if cfg.get('path') is None: cfg['host'] = cfg.get('host', defaults.HOST) cfg['port'] = cfg.get('port', defaults.PORT) cfg['reuse_port'] = cfg.get('reuse_port', defaults.REUSE_PORT) cfg['shutdown_timeout'] = cfg.get('shutdown_timeout', defaults.SHUTDOWN_TIMEOUT) cfg['cache_grace'] = cfg.get('cache_grace', defaults.CACHE_GRACE) if 'cache' not in cfg: cfg['cache'] = {} cfg['cache']['type'] = cfg['cache'].get('type', defaults.CACHE_BACKEND) if cfg['cache']['type'] == 'internal': if 'options' not in cfg['cache']: cfg['cache']['options'] = {} cfg['cache']['options']['cache_size'] = cfg['cache']['options'].\ get('cache_size', defaults.INTERNAL_CACHE_SIZE) def populate_zone(zone): zone['timeout'] = zone.get('timeout', defaults.TIMEOUT) zone['strict_testing'] = zone.get('strict_testing', defaults.STRICT_TESTING) return zone if 'default_zone' not in cfg: cfg['default_zone'] = {} populate_zone(cfg['default_zone']) if 'zones' not in cfg: cfg['zones'] = {} for zone in cfg['zones'].values(): populate_zone(zone) return cfg def load_config(filename): with open(filename, 'rb') as cfg_file: cfg = yaml.safe_load(cfg_file) return populate_cfg_defaults(cfg) def parse_mta_sts_record(rec): return dict(field.partition('=')[0::2] for field in (field.strip() for field in rec.split(';')) if field) def parse_mta_sts_policy(text): lines = text.splitlines() res = dict() res['mx'] = list() for line in lines: line = line.rstrip() key, _, value = line.partition(':') value = value.lstrip() if key == 'mx': res['mx'].append(value) else: res[key] = value return res def is_plaintext(contenttype): return contenttype.lower().partition(';')[0].strip() == 'text/plain' def is_ipaddr(addr): try: socket.getaddrinfo(addr, None, flags=socket.AI_NUMERICHOST) return True except socket.gaierror: return False def filter_domain(domain): lpart, found_separator, rpart = domain.partition(']') res = lpart.lstrip('[') if not found_separator: lpart, found_separator, rpart = domain.rpartition(':') res = lpart if found_separator else rpart return res.lower().strip().rstrip('.') def filter_text(strings): for string in strings: if isinstance(string, str): yield string elif isinstance(string, bytes): try: yield string.decode('ascii') except UnicodeDecodeError: pass else: raise TypeError('Only bytes or strings are expected.') async def create_custom_socket(host, port, *, # pylint: disable=too-many-locals family=socket.AF_UNSPEC, type=socket.SOCK_STREAM, # pylint: disable=redefined-builtin flags=socket.AI_PASSIVE, options=None, loop=None): if loop is None: loop = asyncio.get_event_loop() res = await loop.getaddrinfo(host, port, family=family, type=type, flags=flags) af, s_typ, proto, _, sa = res[0] # pylint: disable=invalid-name sock = socket.socket(af, s_typ, proto) if options is not None: for level, optname, val in options: sock.setsockopt(level, optname, val) sock.bind(sa) return sock def create_cache(cache_type, options): if cache_type == "internal": # pylint: disable=import-outside-toplevel from . import internal_cache cache = internal_cache.InternalLRUCache(**options) elif cache_type == "sqlite": # pylint: disable=import-outside-toplevel from . import sqlite_cache cache = sqlite_cache.SqliteCache(**options) elif cache_type == "redis": # pylint: disable=import-outside-toplevel from . import redis_cache cache = redis_cache.RedisCache(**options) else: raise NotImplementedError("Unsupported cache type!") return cache postfix-mta-sts-resolver-0.7.5/setup.py000066400000000000000000000043521361332105100201550ustar00rootroot00000000000000from os import path from setuptools import setup this_directory = path.abspath(path.dirname(__file__)) # pylint: disable=invalid-name with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: long_description = f.read() # pylint: disable=invalid-name setup(name='postfix_mta_sts_resolver', version='0.7.5', description='Daemon which provides TLS client policy for Postfix ' 'via socketmap, according to domain MTA-STS policy', url='https://github.com/Snawoot/postfix-mta-sts-resolver', author='Vladislav Yarmak', author_email='vladislav-ex-src@vm-0.com', license='MIT', packages=['postfix_mta_sts_resolver'], python_requires='>=3.5.3', setup_requires=[ 'wheel', ], install_requires=[ 'aiodns>=1.1.1', 'aiohttp>=3.4.4', 'PyYAML>=3.12', ], extras_require={ 'sqlite': 'aiosqlite>=0.10.0', 'redis': 'aioredis>=1.2.0', 'dev': [ 'pytest>=3.0.0', 'pytest-cov', 'pytest-asyncio', 'pytest-timeout', 'pylint', 'tox', 'coverage', 'async_generator', 'setuptools>=38.6.0', 'wheel>=0.31.0', 'twine>=1.11.0', 'cryptography>=1.6', ], 'uvloop': 'uvloop>=0.11.0', }, entry_points={ 'console_scripts': [ 'mta-sts-daemon=postfix_mta_sts_resolver.daemon:main', 'mta-sts-query=postfix_mta_sts_resolver.__main__:main', ], }, classifiers=[ "Programming Language :: Python :: 3.5", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Intended Audience :: System Administrators", "Natural Language :: English", "Topic :: Communications :: Email :: Mail Transport Agents", "Topic :: Internet", "Topic :: Security", ], long_description=long_description, long_description_content_type='text/markdown', zip_safe=True) postfix-mta-sts-resolver-0.7.5/tests/000077500000000000000000000000001361332105100176015ustar00rootroot00000000000000postfix-mta-sts-resolver-0.7.5/tests/conftest.py000066400000000000000000000005441361332105100220030ustar00rootroot00000000000000import asyncio import os import pytest from postfix_mta_sts_resolver.utils import enable_uvloop @pytest.fixture(scope="session") def event_loop(): uvloop_test = os.environ['TOXENV'].endswith('-uvloop') uvloop_enabled = enable_uvloop() assert uvloop_test == uvloop_enabled loop = asyncio.get_event_loop() yield loop loop.close() postfix-mta-sts-resolver-0.7.5/tests/dnsmasq.conf.appendix000066400000000000000000000044171361332105100237330ustar00rootroot00000000000000#listen-address=127.0.0.2 #bind-interfaces resolv-file=/etc/resolv-dnsmasq.conf address=/mta-sts.bad-record1.loc/127.0.0.1 txt-record=_mta-sts.bad-record1.loc,"id=20180907T090909; v=STSv1;" address=/mta-sts.bad-record2.loc/127.0.0.1 txt-record=_mta-sts.bad-record2.loc,"azazaza;asdasdasdasd" address=/mta-sts.bad-record3.loc/127.0.0.1 txt-record=_mta-sts.bad-record3.loc,"v=STSv12;id=20180907T090909;" address=/mta-sts.no-record.loc/127.0.0.1 address=/mta-sts.good.loc/127.0.0.1 txt-record=_mta-sts.good.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy1.loc/127.0.0.1 txt-record=_mta-sts.bad-policy1.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy2.loc/127.0.0.1 txt-record=_mta-sts.bad-policy2.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy3.loc/127.0.0.1 txt-record=_mta-sts.bad-policy3.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy4.loc/127.0.0.1 txt-record=_mta-sts.bad-policy4.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy5.loc/127.0.0.1 txt-record=_mta-sts.bad-policy5.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy6.loc/127.0.0.1 txt-record=_mta-sts.bad-policy6.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy7.loc/127.0.0.1 txt-record=_mta-sts.bad-policy7.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-policy8.loc/127.0.0.1 txt-record=_mta-sts.bad-policy8.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-cert1.loc/127.0.0.1 txt-record=_mta-sts.bad-cert1.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.bad-cert2.loc/127.0.0.1 txt-record=_mta-sts.bad-cert2.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.valid-none.loc/127.0.0.1 txt-record=_mta-sts.valid-none.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.testing.loc/127.0.0.1 txt-record=_mta-sts.testing.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.fast-expire.loc/127.0.0.1 txt-record=_mta-sts.fast-expire.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.static-overlength.loc/127.0.0.1 txt-record=_mta-sts.static-overlength.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.chunked-overlength.loc/127.0.0.1 txt-record=_mta-sts.chunked-overlength.loc,"v=STSv1;id=20180907T090909;" address=/mta-sts.no-data.loc/127.0.0.1 address=/_mta-sts.no-data.loc/127.0.0.1 server=/blackhole.loc/240.0.0.1#5354 postfix-mta-sts-resolver-0.7.5/tests/expedite_proxy_startup.sh000077500000000000000000000005461361332105100247770ustar00rootroot00000000000000#!/bin/sh for p in 5 60 60 ; do curl -s -o /dev/null \ -x http://127.0.0.2:1380 \ https://mta-sts.good.loc/.well-known/mta-sts.txt && exit 0 >&2 printf "Proxy startup failed. Restarting in %d seconds..." "$p" sleep "$p" systemctl stop tinyproxy sleep 1 killall -9 tinyproxy systemctl start tinyproxy done exit 1 postfix-mta-sts-resolver-0.7.5/tests/install.debian.sh000077500000000000000000000040731361332105100230330ustar00rootroot00000000000000#!/bin/sh set -e PYTHON="${PYTHON:-python3}" # run under travis, but not under autopkgtest if [ -z "${AUTOPKGTEST_TMP+x}" ] ; then apt-get update apt-get install redis-server dnsmasq lsof nginx-extras tinyproxy -y systemctl start redis-server || { journalctl -xe ; false ; } "$PYTHON" -m pip install cryptography "$PYTHON" -m pip install tox fi install -m 644 tests/resolv.conf /etc/resolv-dnsmasq.conf cat tests/dnsmasq.conf.appendix >> /etc/dnsmasq.conf echo 'nameserver 127.0.0.1' > /etc/resolv.conf systemctl restart dnsmasq || { journalctl -xe ; false ; } # certificates for the test cases mkdir -p /tmp/certs /tmp/bad-certs "$PYTHON" tests/mkcerts.py -o /tmp/certs \ -D good.loc mta-sts.good.loc \ -D bad-policy1.loc mta-sts.bad-policy1.loc \ -D bad-policy2.loc mta-sts.bad-policy2.loc \ -D bad-policy3.loc mta-sts.bad-policy3.loc \ -D bad-policy4.loc mta-sts.bad-policy4.loc \ -D bad-policy5.loc mta-sts.bad-policy5.loc \ -D bad-policy6.loc mta-sts.bad-policy6.loc \ -D bad-policy7.loc mta-sts.bad-policy7.loc \ -D bad-policy8.loc mta-sts.bad-policy8.loc \ -D bad-cert2.loc \ -D valid-none.loc mta-sts.valid-none.loc \ -D mta-sts.testing.loc \ -D chunked-overlength.loc mta-sts.chunked-overlength.loc \ -D static-overlength.loc mta-sts.static-overlength.loc \ -D fast-expire.loc '*.fast-expire.loc' # problematic certificates "$PYTHON" tests/mkcerts.py -o /tmp/bad-certs -D bad-cert1.loc mta-sts.bad-cert1.loc mkdir -p /usr/local/share/ca-certificates/test install -m 644 -o root -g root /tmp/certs/ca.pem /usr/local/share/ca-certificates/test/test-ca.crt update-ca-certificates install -m 644 tests/nginx.conf /etc/nginx/nginx.conf systemctl restart nginx || { journalctl -xe ; false ; } # run under travis, but not under autopkgtest if [ -z "${AUTOPKGTEST_TMP+x}" ] ; then install -m 644 -o root -g root tests/tinyproxy.conf /etc/tinyproxy.conf systemctl restart tinyproxy || { journalctl -xe ; false ; } tests/expedite_proxy_startup.sh || { journalctl -xe ; false ; } else systemctl restart postfix-mta-sts-resolver fi postfix-mta-sts-resolver-0.7.5/tests/mkcerts.py000077500000000000000000000173771361332105100216450ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import datetime import uuid import os.path from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID DAY = datetime.timedelta(1, 0, 0) CA_FILENAME = 'ca' KEY_EXT = 'key' CERT_EXT = 'pem' E = 65537 def parse_args(): def check_keysize(val): def fail(): raise argparse.ArgumentTypeError("%s is not valid key size" % (repr(val),)) try: ival = int(val) except ValueError: fail() if not 1024 <= ival <= 8192: fail() return ival parser = argparse.ArgumentParser( description="Generate RSA certificates signed by common self-signed CA", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("-o", "--output-dir", default='.', help="location of certificates output") parser.add_argument("-k", "--key-size", type=check_keysize, default=2048, help="RSA key size used for all certificates") parser.add_argument("-D", "--domains", action="append", nargs="+", required=True, help="domain names covered by certificate. " "First one will be set as CN. Option can be used " "multiple times") return parser.parse_args() def ensure_private_key(output_dir, name, key_size): key_filename = os.path.join(output_dir, name + '.' + KEY_EXT) if os.path.exists(key_filename): with open(key_filename, "rb") as key_file: private_key = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend()) else: private_key = rsa.generate_private_key(public_exponent=E, key_size=key_size, backend=default_backend()) with open(key_filename, 'wb') as key_file: key_file.write(private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())) return private_key def ensure_ca_key(output_dir, key_size): return ensure_private_key(output_dir, CA_FILENAME, key_size) def ensure_ca_cert(output_dir, ca_private_key): ca_cert_filename = os.path.join(output_dir, CA_FILENAME + '.' + CERT_EXT) ca_public_key = ca_private_key.public_key() if os.path.exists(ca_cert_filename): with open(ca_cert_filename, "rb") as ca_cert_file: ca_cert = x509.load_pem_x509_certificate( ca_cert_file.read(), backend=default_backend()) else: iname = x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, 'Test CA'), x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'postfix-mta-sts-resolver dev'), x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'postfix-mta-sts-resolver testsuite'), ]) ca_cert = x509.CertificateBuilder().\ subject_name(iname).\ issuer_name(iname).\ not_valid_before(datetime.datetime.today() - DAY).\ not_valid_after(datetime.datetime.today() + 3650 * DAY).\ serial_number(x509.random_serial_number()).\ public_key(ca_public_key).\ add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True).\ add_extension( x509.KeyUsage(digital_signature=False, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True, encipher_only=False, decipher_only=False), critical=True).\ add_extension( x509.SubjectKeyIdentifier.from_public_key(ca_public_key), critical=False).\ sign( private_key=ca_private_key, algorithm=hashes.SHA256(), backend=default_backend() ) with open(ca_cert_filename, "wb") as ca_cert_file: ca_cert_file.write( ca_cert.public_bytes(encoding=serialization.Encoding.PEM)) assert isinstance(ca_cert, x509.Certificate) return ca_cert def ensure_end_entity_key(output_dir, name, key_size): return ensure_private_key(output_dir, name, key_size) def ensure_end_entity_cert(output_dir, names, ca_private_key, ca_cert, end_entity_public_key): name = names[0] end_entity_cert_filename = os.path.join(output_dir, name + '.' + CERT_EXT) if os.path.exists(end_entity_cert_filename): return ca_public_key = ca_private_key.public_key() end_entity_cert = x509.CertificateBuilder().\ subject_name(x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, name), ])).\ issuer_name(ca_cert.subject).\ not_valid_before(datetime.datetime.today() - DAY).\ not_valid_after(datetime.datetime.today() + 3650 * DAY).\ serial_number(x509.random_serial_number()).\ public_key(end_entity_public_key).\ add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True).\ add_extension( x509.KeyUsage(digital_signature=True, content_commitment=False, key_encipherment=True, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False), critical=True).\ add_extension( x509.ExtendedKeyUsage([ ExtendedKeyUsageOID.SERVER_AUTH, ExtendedKeyUsageOID.CLIENT_AUTH, ]), critical=False).\ add_extension( x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_public_key), critical=False).\ add_extension( x509.SubjectKeyIdentifier.from_public_key(end_entity_public_key), critical=False).\ add_extension( x509.SubjectAlternativeName( [x509.DNSName(name) for name in names] ), critical=False ).\ sign( private_key=ca_private_key, algorithm=hashes.SHA256(), backend=default_backend() ) with open(end_entity_cert_filename, "wb") as end_entity_cert_file: end_entity_cert_file.write( end_entity_cert.public_bytes(encoding=serialization.Encoding.PEM)) return end_entity_cert def ensure_end_entity_suite(output_dir, names, ca_private_key, ca_cert, key_size): name = names[0] end_entity_key = ensure_end_entity_key(output_dir, name, key_size) end_entity_public_key = end_entity_key.public_key() ensure_end_entity_cert(output_dir, names, ca_private_key, ca_cert, end_entity_public_key) def main(): args = parse_args() ca_private_key = ensure_ca_key(args.output_dir, args.key_size) ca_cert = ensure_ca_cert(args.output_dir, ca_private_key) for names in args.domains: ensure_end_entity_suite(args.output_dir, names, ca_private_key, ca_cert, args.key_size) if __name__ == '__main__': main() postfix-mta-sts-resolver-0.7.5/tests/nginx.conf000066400000000000000000000203531361332105100215760ustar00rootroot00000000000000user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 1024; } http { access_log /var/log/nginx/access.log combined; error_log /var/log/nginx/error.log; include /etc/nginx/mime.types; default_type text/plain; types_hash_max_size 2048; tcp_nopush on; tcp_nodelay on; # Hide Nginx version number server_tokens off; gzip on; keepalive_timeout 65; sendfile on; ssl_protocols TLSv1.2; # Dropping SSLv3, ref: POODLE ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:!3DES:!kRSA"; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; server { server_name mta-sts.good.loc; listen 443 ssl; ssl_certificate /tmp/certs/good.loc.pem; ssl_certificate_key /tmp/certs/good.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-policy1.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy1.loc.pem; ssl_certificate_key /tmp/certs/bad-policy1.loc.key; location = /.well-known/mta-sts.txt { return 200 "garbage"; } location / { return 404; } } server { server_name mta-sts.bad-policy2.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy2.loc.pem; ssl_certificate_key /tmp/certs/bad-policy2.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-policy3.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy3.loc.pem; ssl_certificate_key /tmp/certs/bad-policy3.loc.key; location = /.well-known/mta-sts.txt { types { text/html txt; } return 200 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-policy4.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy4.loc.pem; ssl_certificate_key /tmp/certs/bad-policy4.loc.key; location = /.well-known/mta-sts.txt { return 404 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-policy5.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy5.loc.pem; ssl_certificate_key /tmp/certs/bad-policy5.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: xxx"; } location / { return 404; } } server { server_name mta-sts.bad-policy6.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy6.loc.pem; ssl_certificate_key /tmp/certs/bad-policy6.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: 31557601"; } location / { return 404; } } server { server_name mta-sts.bad-policy7.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy7.loc.pem; ssl_certificate_key /tmp/certs/bad-policy7.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-policy8.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-policy8.loc.pem; ssl_certificate_key /tmp/certs/bad-policy8.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: doubt\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-cert1.loc; listen 443 ssl; ssl_certificate /tmp/bad-certs/bad-cert1.loc.pem; ssl_certificate_key /tmp/bad-certs/bad-cert1.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.bad-cert2.loc; listen 443 ssl; ssl_certificate /tmp/certs/bad-cert2.loc.pem; ssl_certificate_key /tmp/certs/bad-cert2.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmx: mail.loc\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.valid-none.loc; listen 443 ssl; ssl_certificate /tmp/certs/valid-none.loc.pem; ssl_certificate_key /tmp/certs/valid-none.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: none\nmax_age: 86400"; } location / { return 404; } } server { server_name mta-sts.testing.loc; listen 443 ssl; ssl_certificate /tmp/certs/mta-sts.testing.loc.pem; ssl_certificate_key /tmp/certs/mta-sts.testing.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: testing\nmax_age: 86400\nmx: mail.loc\n"; } location / { return 404; } } server { server_name mta-sts.fast-expire.loc; listen 443 ssl; ssl_certificate /tmp/certs/fast-expire.loc.pem; ssl_certificate_key /tmp/certs/fast-expire.loc.key; location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmax_age: 1\nmx: mail.loc\n"; } location / { return 404; } } server { server_name mta-sts.chunked-overlength.loc; listen 443 ssl; ssl_certificate /tmp/certs/chunked-overlength.loc.pem; ssl_certificate_key /tmp/certs/chunked-overlength.loc.key; location = /.well-known/mta-sts.txt { content_by_lua_block { ngx.say("version: STSv1") ngx.say("mode: enforce") ngx.say("max_age: 86400") ngx.say("mx: mail.loc") ngx.flush(true) for i=0, 2048, 1 do ngx.print("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") ngx.flush(true) end } } location / { return 404; } } server { server_name mta-sts.static-overlength.loc; listen 443 ssl; ssl_certificate /tmp/certs/static-overlength.loc.pem; ssl_certificate_key /tmp/certs/static-overlength.loc.key; location = /.well-known/mta-sts.txt { lua_http10_buffering off; content_by_lua_block { ngx.header["content-length"] = "131193" ngx.send_headers() ngx.say("version: STSv1") ngx.say("mode: enforce") ngx.say("max_age: 86400") ngx.say("mx: mail.loc") ngx.flush(true) for i=0, 2048, 1 do ngx.print("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") ngx.flush(true) end } } location / { return 404; } } } postfix-mta-sts-resolver-0.7.5/tests/refdata.tsv000066400000000000000000000012661361332105100217520ustar00rootroot00000000000000test good.loc OK secure match=mail.loc test [good.loc] OK secure match=mail.loc test [good.loc]:123 OK secure match=mail.loc test good.loc:123 OK secure match=mail.loc test2 good.loc OK secure match=mail.loc test good.loc. OK secure match=mail.loc test .good.loc NOTFOUND test valid-none.loc NOTFOUND test testing.loc NOTFOUND test no-record.loc NOTFOUND test [no-record.loc] NOTFOUND test .no-record.loc NOTFOUND test [1.2.3.4] NOTFOUND test [a:bb:ccc::dddd]:123 NOTFOUND test bad-record1.loc NOTFOUND test bad-record2.loc NOTFOUND test bad-policy1.loc NOTFOUND test bad-policy2.loc NOTFOUND test bad-policy3.loc NOTFOUND test bad-cert1.loc NOTFOUND test bad-cert2.loc NOTFOUND postfix-mta-sts-resolver-0.7.5/tests/refdata_strict.tsv000066400000000000000000000013051361332105100233340ustar00rootroot00000000000000test good.loc OK secure match=mail.loc test [good.loc] OK secure match=mail.loc test [good.loc]:123 OK secure match=mail.loc test good.loc:123 OK secure match=mail.loc test2 good.loc OK secure match=mail.loc test good.loc. OK secure match=mail.loc test .good.loc NOTFOUND test valid-none.loc NOTFOUND test testing.loc OK secure match=mail.loc test no-record.loc NOTFOUND test [no-record.loc] NOTFOUND test .no-record.loc NOTFOUND test [1.2.3.4] NOTFOUND test [a:bb:ccc::dddd]:123 NOTFOUND test bad-record1.loc NOTFOUND test bad-record2.loc NOTFOUND test bad-policy1.loc NOTFOUND test bad-policy2.loc NOTFOUND test bad-policy3.loc NOTFOUND test bad-cert1.loc NOTFOUND test bad-cert2.loc NOTFOUND postfix-mta-sts-resolver-0.7.5/tests/resolv.conf000066400000000000000000000000461361332105100217620ustar00rootroot00000000000000nameserver 1.1.1.1 nameserver 1.0.0.1 postfix-mta-sts-resolver-0.7.5/tests/test_asdnotify.py000066400000000000000000000071211361332105100232130ustar00rootroot00000000000000import contextlib import socket import asyncio import os import sys import pytest from postfix_mta_sts_resolver.asdnotify import AsyncSystemdNotifier @contextlib.contextmanager def set_env(**environ): old_environ = dict(os.environ) os.environ.update(environ) try: yield finally: os.environ.clear() os.environ.update(old_environ) class UnixDatagramReceiver: def __init__(self, loop): self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self._sock.setblocking(0) self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._sock.bind('') self._name = self._sock.getsockname() self._incoming = asyncio.Queue() self._loop = loop loop.add_reader(self._sock.fileno(), self._read_handler) def _read_handler(self): try: while True: msg = self._sock.recv(4096) self._incoming.put_nowait(msg) except BlockingIOError: # pragma: no cover pass async def recvmsg(self): return await self._incoming.get() @property def name(self): return self._name @property def asciiname(self): sockname = self.name if isinstance(sockname, bytes): sockname = sockname.decode('ascii') if sockname.startswith('\x00'): sockname = '@' + sockname[1:] return sockname def close(self): self._loop.remove_reader(self._sock.fileno()) self._sock.close() self._sock = None pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") @pytest.fixture(scope="module") def unix_dgram_receiver(event_loop): udr = UnixDatagramReceiver(event_loop) yield udr udr.close() @pytest.mark.timeout(5) @pytest.mark.asyncio async def test_message_sent(unix_dgram_receiver): sockname = unix_dgram_receiver.asciiname msg = b"READY=1" with set_env(NOTIFY_SOCKET=sockname): async with AsyncSystemdNotifier() as notifier: await notifier.notify(msg) assert await unix_dgram_receiver.recvmsg() == msg @pytest.mark.timeout(5) @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") @pytest.mark.asyncio async def test_message_flow(unix_dgram_receiver): sockname = unix_dgram_receiver.asciiname msgs = [b"READY=1", b'STOPPING=1'] * 500 with set_env(NOTIFY_SOCKET=sockname): async with AsyncSystemdNotifier() as notifier: for msg in msgs: await notifier.notify(msg) assert await unix_dgram_receiver.recvmsg() == msg @pytest.mark.timeout(5) @pytest.mark.asyncio async def test_not_started(): async with AsyncSystemdNotifier() as notifier: assert not notifier.started @pytest.mark.timeout(5) @pytest.mark.asyncio async def test_started(unix_dgram_receiver): with set_env(NOTIFY_SOCKET=unix_dgram_receiver.asciiname): async with AsyncSystemdNotifier() as notifier: assert notifier.started @pytest.mark.timeout(5) @pytest.mark.asyncio async def test_send_never_fails(): with set_env(NOTIFY_SOCKET='abc'): async with AsyncSystemdNotifier() as notifier: await notifier.notify(b'!!!') @pytest.mark.timeout(5) @pytest.mark.asyncio async def test_socket_create_failure(monkeypatch): class mocksock: def __init__(self, *args, **kwargs): raise OSError() monkeypatch.setattr(socket, "socket", mocksock) with set_env(NOTIFY_SOCKET='abc'): async with AsyncSystemdNotifier() as notifier: await notifier.notify(b'!!!') postfix-mta-sts-resolver-0.7.5/tests/test_cache.py000066400000000000000000000026631361332105100222640ustar00rootroot00000000000000import tempfile import pytest import postfix_mta_sts_resolver.utils as utils import postfix_mta_sts_resolver.base_cache as base_cache @pytest.mark.parametrize("cache_type,cache_opts", [ ("internal", {}), ("sqlite", {}), ("redis", {"address": "redis://127.0.0.1/0?timeout=5"}), ]) @pytest.mark.asyncio async def test_cache_lifecycle(cache_type, cache_opts): if cache_type == 'sqlite': tmpfile = tempfile.NamedTemporaryFile() cache_opts["filename"] = tmpfile.name cache = utils.create_cache(cache_type, cache_opts) await cache.setup() assert await cache.get("nonexistent") == None stored = base_cache.CacheEntry(0, "pol_id", "pol_body") await cache.set("test", stored) await cache.set("test", stored) # second time for testing conflicting insert assert await cache.get("test") == stored await cache.teardown() if cache_type == 'sqlite': tmpfile.close() @pytest.mark.asyncio async def test_capped_cache(): cache = utils.create_cache("internal", {"cache_size": 2}) await cache.setup() stored = base_cache.CacheEntry(0, "pol_id", "pol_body") await cache.set("test1", stored) await cache.set("test2", stored) await cache.set("test3", stored) assert await cache.get("test2") == stored assert await cache.get("test3") == stored def test_unknown_cache_lifecycle(): with pytest.raises(NotImplementedError): cache = utils.create_cache("void", {}) postfix-mta-sts-resolver-0.7.5/tests/test_daemon.py000066400000000000000000000020211361332105100224500ustar00rootroot00000000000000import sys import asyncio import argparse import pytest import postfix_mta_sts_resolver.daemon as daemon import postfix_mta_sts_resolver.utils as utils class MockCmdline: def __init__(self, *args): self._cmdline = args def __enter__(self): self._old_cmdline = sys.argv sys.argv = list(self._cmdline) def __exit__(self, exc_type, exc_value, traceback): sys.argv = self._old_cmdline @pytest.mark.asyncio async def test_heartbeat(): with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(daemon.heartbeat(), 1.5) def test_parse_args(): with MockCmdline("mta-sts-daemon", "-c", "/dev/null", "-v", "info"): args = daemon.parse_args() assert args.config == '/dev/null' assert not args.disable_uvloop assert args.verbosity == utils.LogLevel.info assert args.logfile is None def test_bad_args(): with MockCmdline("mta-sts-daemon", "-c", "/dev/null", "-v", "xxx"): with pytest.raises(SystemExit): args = daemon.parse_args() postfix-mta-sts-resolver-0.7.5/tests/test_main.py000066400000000000000000000013571361332105100221440ustar00rootroot00000000000000import sys import pytest import postfix_mta_sts_resolver.__main__ as main class MockCmdline: def __init__(self, *args): self._cmdline = args def __enter__(self): self._old_cmdline = sys.argv sys.argv = list(self._cmdline) def __exit__(self, exc_type, exc_value, traceback): sys.argv = self._old_cmdline def test_parse_args(): with MockCmdline("mta-sts-query", "example.com"): args = main.parse_args() assert args.domain == 'example.com' assert args.known_version is None def test_parse_args_with_version(): with MockCmdline("mta-sts-query", "example.com", "123"): args = main.parse_args() assert args.domain == 'example.com' assert args.known_version == "123" postfix-mta-sts-resolver-0.7.5/tests/test_netstring.py000066400000000000000000000104231361332105100232270ustar00rootroot00000000000000import pytest import postfix_mta_sts_resolver.netstring as netstring @pytest.mark.parametrize("reference,sample", [ pytest.param(b'0:,', b'', id="empty_str"), pytest.param(b'1:X,', b'X', id="single_byte"), ]) def test_encode(reference, sample): assert reference == netstring.encode(sample) @pytest.mark.parametrize("reference,sample", [ pytest.param([b'',], b'00:,', id="null_string"), pytest.param([b'X',], b'01:X,', id="single_byte"), pytest.param([b'',b'X'], b'00:,1:X,', id="null_string_with_continuation"), pytest.param([b'X',b'X'], b'01:X,1:X,', id="single_byte_with_continuation"), ]) def test_leading_zeroes(reference, sample): assert reference == list(netstring.decode(sample)) @pytest.mark.parametrize("reference,sample", [ pytest.param([], b'', id="nodata"), pytest.param([b''], b'0:,', id="empty"), pytest.param([b'5:Hello,6:World!,'], b'17:5:Hello,6:World!,,', id="nested"), ]) def test_decode(reference, sample): assert reference == list(netstring.decode(sample)) @pytest.mark.parametrize("encoded", [b':,', b'aaa:aaa']) def test_bad_length(encoded): with pytest.raises(netstring.BadLength): list(netstring.decode(encoded)) @pytest.mark.parametrize("encoded", [b'3', b'3:', b'3:a', b'3:aa', b'3:aaa']) def test_decode_incomplete_string(encoded): with pytest.raises(netstring.IncompleteNetstring): list(netstring.decode(encoded)) def test_abandoned_string_reader_handles(): stream_reader = netstring.StreamReader() stream_reader.feed(b'0:,') string_reader = stream_reader.next_string() with pytest.raises(netstring.InappropriateParserState): string_reader = stream_reader.next_string() @pytest.mark.parametrize("encoded", [b'0:_', b'3:aaa_']) def test_bad_terminator(encoded): with pytest.raises(netstring.BadTerminator): list(netstring.decode(encoded)) @pytest.mark.parametrize("reference,sequence", [ pytest.param([b''], [b'0', b':', b','], id="empty"), pytest.param([b'X', b'abc', b'ok'], [b'1:', b'X,3:abc,2:', b'ok,'], id="multiple_and_partial"), pytest.param([b'X', b'123456789', b'ok'], [b'1:', b'X,9:123', b'456', b'789', b',2:', b'ok,'], id="multiple_and_partial2"), ]) def test_stream_reader(reference, sequence): incoming = sequence[::-1] results = [] stream_reader = netstring.StreamReader() while incoming: string_reader = stream_reader.next_string() res = b'' while True: try: buf = string_reader.read() except netstring.WantRead: if incoming: stream_reader.feed(incoming.pop()) else: break else: if not buf: break res += buf results.append(res) assert results == reference def test_stream_portions(): stream_reader = netstring.StreamReader() string_reader = stream_reader.next_string() with pytest.raises(netstring.WantRead): string_reader.read() stream_reader.feed(b'1:') with pytest.raises(netstring.WantRead): string_reader.read() stream_reader.feed(b'X,9:123') assert string_reader.read() == b'X' assert string_reader.read() == b'' string_reader = stream_reader.next_string() assert string_reader.read() == b'123' stream_reader.feed(b'456') assert string_reader.read() == b'456' stream_reader.feed(b'789') assert string_reader.read() == b'789' with pytest.raises(netstring.WantRead): string_reader.read() stream_reader.feed(b',2:') assert string_reader.read() == b'' string_reader = stream_reader.next_string() with pytest.raises(netstring.WantRead): string_reader.read() stream_reader.feed(b'ok,') assert string_reader.read() == b'ok' assert string_reader.read() == b'' string_reader = stream_reader.next_string() with pytest.raises(netstring.WantRead): string_reader.read() @pytest.mark.parametrize("limit,sample", [ (5, b'6:123456,'), (1024, b'9999:aaa'), (1024, b'9999:'), (1024, b'9999'), ]) def test_limit(limit, sample): stream_reader = netstring.StreamReader(limit) stream_reader.feed(sample) string_reader = stream_reader.next_string() with pytest.raises(netstring.TooLong): string_reader.read() postfix-mta-sts-resolver-0.7.5/tests/test_resolver.py000066400000000000000000000110471361332105100230560ustar00rootroot00000000000000import collections.abc import contextlib import os import pytest import postfix_mta_sts_resolver.resolver as resolver from postfix_mta_sts_resolver.resolver import STSFetchResult as FR from postfix_mta_sts_resolver.resolver import STSResolver as Resolver @contextlib.contextmanager def set_env(**environ): old_environ = dict(os.environ) os.environ.update(environ) try: yield finally: os.environ.clear() os.environ.update(old_environ) @pytest.mark.parametrize("domain", ['good.loc', 'good.loc.']) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_simple_resolve(domain): resolver = Resolver(loop=None, timeout=1) status, (ver, policy) = await resolver.resolve(domain) assert status is FR.VALID assert 'mx' in policy assert isinstance(policy['mx'], collections.abc.Iterable) assert all(isinstance(dom, str) for dom in policy['mx']) assert policy['version'] == 'STSv1' assert policy['mode'] in ('none', 'enforce', 'testing') assert isinstance(policy['max_age'], int) assert policy['max_age'] > 0 assert isinstance(ver, str) assert ver status, body2 = await resolver.resolve(domain, ver) assert status is FR.NOT_CHANGED assert body2 is None @pytest.mark.parametrize("domain,expected_status", [("good.loc", FR.VALID), ("good.loc.", FR.VALID), ("testing.loc", FR.VALID), (".good.loc", FR.NONE), (".good.loc.", FR.NONE), ("valid-none.loc", FR.VALID), ("no-record.loc", FR.NONE), ("no-data.loc", FR.NONE), ("bad-record1.loc", FR.NONE), ("bad-record2.loc", FR.NONE), ("bad-record3.loc", FR.NONE), ("bad-policy1.loc", FR.FETCH_ERROR), ("bad-policy2.loc", FR.FETCH_ERROR), ("bad-policy3.loc", FR.FETCH_ERROR), ("bad-policy4.loc", FR.FETCH_ERROR), ("bad-policy5.loc", FR.FETCH_ERROR), ("bad-policy6.loc", FR.FETCH_ERROR), ("bad-policy7.loc", FR.FETCH_ERROR), ("bad-policy8.loc", FR.FETCH_ERROR), ("static-overlength.loc", FR.FETCH_ERROR), ("chunked-overlength.loc", FR.FETCH_ERROR), ("bad-cert1.loc", FR.FETCH_ERROR), ("bad-cert2.loc", FR.FETCH_ERROR), ]) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_resolve_status(event_loop, domain, expected_status): resolver = Resolver(loop=event_loop, timeout=1) status, body = await resolver.resolve(domain) assert status is expected_status if expected_status is FR.VALID: ver, pol = body if pol['mode'] != 'none': assert isinstance(pol['mx'], collections.abc.Iterable) assert pol['mx'] else: assert body is None @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_resolve_dns_timeout(event_loop): resolver = Resolver(loop=event_loop, timeout=1) status, body = await resolver.resolve('blackhole.loc') assert status is FR.FETCH_ERROR assert body is None @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_proxy(event_loop): with set_env(https_proxy='http://127.0.0.2:1380'): resolver = Resolver(loop=event_loop) status, (ver, pol) = await resolver.resolve("good.loc") assert status is FR.VALID assert pol['mode'] == 'enforce' assert pol['mx'] == ['mail.loc'] @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_proxy_negative(event_loop): with set_env(https_proxy='http://127.0.0.2:18888'): resolver = Resolver(loop=event_loop) status, body = await resolver.resolve("good.loc") assert status is FR.FETCH_ERROR assert body is None postfix-mta-sts-resolver-0.7.5/tests/test_responder.py000066400000000000000000000154631361332105100232240ustar00rootroot00000000000000import sys import asyncio import itertools import socket import os import pytest from postfix_mta_sts_resolver import netstring from postfix_mta_sts_resolver.responder import STSSocketmapResponder import postfix_mta_sts_resolver.utils as utils from async_generator import yield_, async_generator from testdata import load_testdata @pytest.fixture(scope="module") @async_generator async def responder(event_loop): import postfix_mta_sts_resolver.utils as utils cfg = utils.populate_cfg_defaults(None) cfg["zones"]["test2"] = cfg["default_zone"] resp = STSSocketmapResponder(cfg, event_loop) await resp.start() result = resp, cfg['host'], cfg['port'] await yield_(result) await resp.stop() @pytest.fixture(scope="module") @async_generator async def unix_responder(event_loop): import postfix_mta_sts_resolver.utils as utils cfg = utils.populate_cfg_defaults({'path': '/tmp/mta-sts.sock', 'mode': 0o666}) cfg["zones"]["test2"] = cfg["default_zone"] resp = STSSocketmapResponder(cfg, event_loop) await resp.start() result = resp, cfg['path'] await yield_(result) await resp.stop() buf_sizes = [4096, 128, 16, 1] reqresps = list(load_testdata('refdata')) bufreq_pairs = tuple(itertools.product(reqresps, buf_sizes)) @pytest.mark.parametrize("params", bufreq_pairs) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_responder(responder, params): (request, response), bufsize = params resp, host, port = responder stream_reader = netstring.StreamReader() string_reader = stream_reader.next_string() reader, writer = await asyncio.open_connection(host, port) try: writer.write(netstring.encode(request)) res = b'' while True: try: part = string_reader.read() except netstring.WantRead: buf = await reader.read(bufsize) assert buf stream_reader.feed(buf) else: if not part: break res += part assert res == response finally: writer.close() @pytest.mark.parametrize("params", tuple(itertools.product(reqresps, buf_sizes))) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_unix_responder(unix_responder, params): (request, response), bufsize = params resp, path = unix_responder stream_reader = netstring.StreamReader() string_reader = stream_reader.next_string() assert os.stat(path).st_mode & 0o777 == 0o666 reader, writer = await asyncio.open_unix_connection(path) try: writer.write(netstring.encode(request)) res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(bufsize) assert data stream_reader.feed(data) else: if not part: break res += part assert res == response finally: writer.close() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_empty_dialog(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) writer.close() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_corrupt_dialog(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) msg = netstring.encode(b'test good.loc')[:-1] + b'!' writer.write(msg) assert await reader.read() == b'' writer.close() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_early_disconnect(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) writer.write(netstring.encode(b'test good.loc')) writer.close() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_cached(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) stream_reader = netstring.StreamReader() writer.write(netstring.encode(b'test good.loc')) writer.write(netstring.encode(b'test good.loc')) answers = [] try: for _ in range(2): string_reader = stream_reader.next_string() res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(4096) assert data stream_reader.feed(data) else: if not part: break res += part answers.append(res) assert answers[0] == answers[1] finally: writer.close() @pytest.mark.asyncio @pytest.mark.timeout(7) async def test_fast_expire(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) stream_reader = netstring.StreamReader() async def answer(): string_reader = stream_reader.next_string() res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(4096) assert data stream_reader.feed(data) else: if not part: break res += part return res try: writer.write(netstring.encode(b'test fast-expire.loc')) answer_a = await answer() await asyncio.sleep(2) writer.write(netstring.encode(b'test fast-expire.loc')) answer_b = await answer() assert answer_a == answer_b == b'OK secure match=mail.loc' finally: writer.close() @pytest.mark.parametrize("params", tuple(itertools.product(reqresps, buf_sizes))) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_responder_with_custom_socket(event_loop, responder, params): (request, response), bufsize = params resp, host, port = responder sock = await utils.create_custom_socket(host, 0, flags=0, options=[(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)]) stream_reader = netstring.StreamReader() string_reader = stream_reader.next_string() await event_loop.run_in_executor(None, sock.connect, (host, port)) reader, writer = await asyncio.open_connection(sock=sock) try: writer.write(netstring.encode(request)) res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(bufsize) assert data stream_reader.feed(data) else: if not part: break res += part assert res == response finally: writer.close() postfix-mta-sts-resolver-0.7.5/tests/test_responder_expiration.py000066400000000000000000000045461361332105100254660ustar00rootroot00000000000000import asyncio import tempfile import os import contextlib import pytest from postfix_mta_sts_resolver import netstring from postfix_mta_sts_resolver.responder import STSSocketmapResponder import postfix_mta_sts_resolver.utils as utils import postfix_mta_sts_resolver.base_cache as base_cache @contextlib.contextmanager def set_env(**environ): old_environ = dict(os.environ) os.environ.update(environ) try: yield finally: os.environ.clear() os.environ.update(old_environ) @pytest.mark.asyncio @pytest.mark.timeout(10) async def test_responder_expiration(event_loop): async def query(host, port, domain): reader, writer = await asyncio.open_connection(host, port) stream_reader = netstring.StreamReader() string_reader = stream_reader.next_string() writer.write(netstring.encode(b'test ' + domain.encode('ascii'))) try: res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(4096) assert data stream_reader.feed(data) else: if not part: break res += part return res finally: writer.close() with tempfile.NamedTemporaryFile() as cachedb: cfg = {} cfg["port"] = 18461 cfg["cache_grace"] = 0 cfg["shutdown_timeout"] = 1 cfg["cache"] = { "type": "sqlite", "options": { "filename": cachedb.name, }, } cfg = utils.populate_cfg_defaults(cfg) cache = utils.create_cache(cfg['cache']['type'], cfg['cache']['options']) await cache.setup() pol_body = { "version": "STSv1", "mode": "enforce", "mx": [ "mail.loc" ], "max_age": 1, } await cache.set("no-record.loc", base_cache.CacheEntry(0, "0", pol_body)) await cache.teardown() resp = STSSocketmapResponder(cfg, event_loop) await resp.start() try: result = await query(cfg['host'], cfg['port'], 'no-record.loc') assert result == b'NOTFOUND ' finally: await resp.stop() postfix-mta-sts-resolver-0.7.5/tests/test_responder_strict.py000066400000000000000000000033161361332105100246060ustar00rootroot00000000000000import sys import asyncio import itertools import socket import pytest from postfix_mta_sts_resolver import netstring from postfix_mta_sts_resolver.responder import STSSocketmapResponder import postfix_mta_sts_resolver.utils as utils from async_generator import yield_, async_generator from testdata import load_testdata @pytest.fixture(scope="module") @async_generator async def responder(event_loop): import postfix_mta_sts_resolver.utils as utils cfg = utils.populate_cfg_defaults({"default_zone": {"strict_testing": True}}) cfg["zones"]["test2"] = cfg["default_zone"] cfg["port"] = 28461 resp = STSSocketmapResponder(cfg, event_loop) await resp.start() result = resp, cfg['host'], cfg['port'] await yield_(result) await resp.stop() buf_sizes = [4096, 128, 16, 1] reqresps = list(load_testdata('refdata_strict')) @pytest.mark.parametrize("params", tuple(itertools.product(reqresps, buf_sizes))) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_responder(responder, params): (request, response), bufsize = params resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) stream_reader = netstring.StreamReader() string_reader = stream_reader.next_string() try: writer.write(netstring.encode(request)) res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(bufsize) assert data stream_reader.feed(data) else: if not part: break res += part assert res == response finally: writer.close() postfix-mta-sts-resolver-0.7.5/tests/test_responder_volatile.py000066400000000000000000000074241361332105100251210ustar00rootroot00000000000000import sys import asyncio import itertools import socket import pytest from postfix_mta_sts_resolver import netstring from postfix_mta_sts_resolver.responder import STSSocketmapResponder import postfix_mta_sts_resolver.utils as utils from async_generator import yield_, async_generator @pytest.fixture @async_generator async def responder(event_loop): import postfix_mta_sts_resolver.utils as utils cfg = utils.populate_cfg_defaults(None) cfg["port"] = 38461 cfg["shutdown_timeout"] = 1 cfg["cache_grace"] = 0 cfg["zones"]["test2"] = cfg["default_zone"] resp = STSSocketmapResponder(cfg, event_loop) await resp.start() result = resp, cfg['host'], cfg['port'] await yield_(result) await resp.stop() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_hanging_stop(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) await resp.stop() assert await reader.read() == b'' writer.close() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_inprogress_stop(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) writer.write(netstring.encode(b'test blackhole.loc')) await writer.drain() await asyncio.sleep(0.2) await resp.stop() assert await reader.read() == b'' writer.close() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_extended_stop(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) writer.write(netstring.encode(b'test blackhole.loc')) writer.write(netstring.encode(b'test blackhole.loc')) writer.write(netstring.encode(b'test blackhole.loc')) await writer.drain() await asyncio.sleep(0.2) await resp.stop() assert await reader.read() == b'' writer.close() @pytest.mark.asyncio @pytest.mark.timeout(7) async def test_grace_expired(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) stream_reader = netstring.StreamReader() async def answer(): string_reader = stream_reader.next_string() res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(4096) assert data stream_reader.feed(data) else: if not part: break res += part return res try: writer.write(netstring.encode(b'test good.loc')) answer_a = await answer() await asyncio.sleep(2) writer.write(netstring.encode(b'test good.loc')) answer_b = await answer() assert answer_a == answer_b finally: writer.close() @pytest.mark.asyncio @pytest.mark.timeout(7) async def test_fast_expire(responder): resp, host, port = responder reader, writer = await asyncio.open_connection(host, port) stream_reader = netstring.StreamReader() async def answer(): string_reader = stream_reader.next_string() res = b'' while True: try: part = string_reader.read() except netstring.WantRead: data = await reader.read(4096) assert data stream_reader.feed(data) else: if not part: break res += part return res try: writer.write(netstring.encode(b'test fast-expire.loc')) answer_a = await answer() await asyncio.sleep(2) writer.write(netstring.encode(b'test fast-expire.loc')) answer_b = await answer() assert answer_a == answer_b == b'OK secure match=mail.loc' finally: writer.close() postfix-mta-sts-resolver-0.7.5/tests/test_sqliteconnpool.py000066400000000000000000000070661361332105100242740ustar00rootroot00000000000000import asyncio import tempfile import pytest from postfix_mta_sts_resolver.sqlite_cache import SqliteConnPool CONN_INIT = [ "PRAGMA journal_mode=WAL", "PRAGMA synchronous=NORMAL", ] @pytest.fixture def dbfile(): with tempfile.NamedTemporaryFile() as f: yield f.name @pytest.mark.asyncio async def test_raises_re(dbfile): pool = SqliteConnPool(1, (dbfile,), init_queries=CONN_INIT) with pytest.raises(RuntimeError): async with pool.borrow() as conn: pass @pytest.mark.asyncio @pytest.mark.timeout(2) async def test_basic(dbfile): pool = SqliteConnPool(1, (dbfile,), init_queries=CONN_INIT) await pool.prepare() try: async with pool.borrow() as conn: await conn.execute("create table if not exists t (id int)") async with pool.borrow() as conn: for i in range(10): await conn.execute("insert into t (id) values (?)", (i,)) await conn.commit() async with pool.borrow() as conn: async with conn.execute('select sum(id) from t') as cur: res = await cur.fetchone() assert res[0] == sum(range(10)) finally: await pool.stop() @pytest.mark.asyncio @pytest.mark.timeout(2) async def test_early_stop(dbfile): pool = SqliteConnPool(5, (dbfile,), init_queries=CONN_INIT) await pool.prepare() async with pool.borrow() as conn: await pool.stop() await conn.execute("create table if not exists t (id int)") for i in range(10): await conn.execute("insert into t (id) values (?)", (i,)) await conn.commit() async with conn.execute('select sum(id) from t') as cur: res = await cur.fetchone() assert res[0] == sum(range(10)) @pytest.mark.asyncio @pytest.mark.timeout(2) async def test_borrow_timeout(dbfile): pool = SqliteConnPool(1, (dbfile,), init_queries=CONN_INIT) await pool.prepare() try: async with pool.borrow(1) as conn1: with pytest.raises(asyncio.TimeoutError): async with pool.borrow(1) as conn2: pass finally: await pool.stop() @pytest.mark.asyncio @pytest.mark.timeout(2) async def test_conn_reuse(dbfile): pool = SqliteConnPool(1, (dbfile,), init_queries=CONN_INIT) await pool.prepare() try: async with pool.borrow() as conn: first = conn async with pool.borrow() as conn: second = conn assert first is second finally: await pool.stop() @pytest.mark.asyncio @pytest.mark.timeout(2) async def test_conn_ressurection(dbfile): class TestError(Exception): pass pool = SqliteConnPool(1, (dbfile,), init_queries=CONN_INIT) await pool.prepare() try: with pytest.raises(TestError): async with pool.borrow() as conn: first = conn async with conn.execute("SELECT 1") as cur: result = await cur.fetchone() assert result[0] == 1 raise TestError() async with pool.borrow() as conn: async with conn.execute("SELECT 1") as cur: result = await cur.fetchone() assert result[0] == 1 second = conn assert first is not second finally: await pool.stop() @pytest.mark.asyncio @pytest.mark.timeout(2) async def test_bad_init(dbfile): pool = SqliteConnPool(1, (dbfile,), init_queries=['BOGUSQUERY']) try: with pytest.raises(Exception): await pool.prepare() finally: await pool.stop() postfix-mta-sts-resolver-0.7.5/tests/test_utils.py000066400000000000000000000112351361332105100223540ustar00rootroot00000000000000import tempfile import collections.abc import enum import itertools import time import pytest import postfix_mta_sts_resolver.utils as utils @pytest.mark.parametrize("cfg", [None, {}, { "zones": { "aaa": {}, "bbb": {}, } }, ]) def test_populate_cfg_defaults(cfg): res = utils.populate_cfg_defaults(cfg) assert isinstance(res['host'], str) assert isinstance(res['port'], int) assert 0 < res['port'] < 65536 assert isinstance(res['cache_grace'], (int, float)) assert isinstance(res['cache'], collections.abc.Mapping) assert res['cache']['type'] in ('redis', 'sqlite', 'internal') assert isinstance(res['default_zone'], collections.abc.Mapping) assert isinstance(res['zones'], collections.abc.Mapping) for zone in list(res['zones'].values()) + [res['default_zone']]: assert isinstance(zone, collections.abc.Mapping) assert 'timeout' in zone assert 'strict_testing' in zone def test_empty_config(): assert utils.load_config('/dev/null') == utils.populate_cfg_defaults(None) @pytest.mark.parametrize("rec,expected", [ ("v=STSv1; id=20160831085700Z;", {"v": "STSv1", "id": "20160831085700Z"}), ("v=STSv1;id=20160831085700Z;", {"v": "STSv1", "id": "20160831085700Z"}), ("v=STSv1; id=20160831085700Z", {"v": "STSv1", "id": "20160831085700Z"}), ("v=STSv1;id=20160831085700Z", {"v": "STSv1", "id": "20160831085700Z"}), ("v=STSv1; id=20160831085700Z ", {"v": "STSv1", "id": "20160831085700Z"}), ("", {}), (" ", {}), (" ; ; ", {}), ("v=STSv1; id=20160831085700Z;;;", {"v": "STSv1", "id": "20160831085700Z"}), ]) def test_parse_mta_sts_record(rec, expected): assert utils.parse_mta_sts_record(rec) == expected @pytest.mark.parametrize("contenttype,expected", [ ("text/plain", True), ("TEXT/PLAIN", True), ("TeXT/PlAiN", True), ("text/plain;charset=utf-8", True), ("text/plain;charset=UTF-8", True), ("text/plain; charset=UTF-8", True), ("text/plain ; charset=UTF-8", True), ("application/octet-stream", False), ("application/octet-stream+text/plain", False), ("application/json+text/plain", False), ("text/plain+", False), ]) def test_is_plaintext(contenttype, expected): assert utils.is_plaintext(contenttype) == expected class TextType(enum.Enum): ascii_byte_string = 1 nonascii_byte_string = 2 unicode_string = 3 invalid_string = 4 text_args = [ (b"aaa", TextType.ascii_byte_string), (b"\xff", TextType.nonascii_byte_string), ("aaa", TextType.unicode_string), (None, TextType.invalid_string), (0, TextType.invalid_string), ] text_params = [] for length in range(0, 5): text_params.extend(itertools.product(text_args, repeat=length)) @pytest.mark.parametrize("vector", text_params) def test_filter_text(vector): if any(typ is TextType.invalid_string for (_, typ) in vector): with pytest.raises(TypeError): for _ in utils.filter_text(val for (val, _) in vector): pass else: res = list(utils.filter_text(val for (val, _) in vector)) nonskipped = (pair for pair in vector if pair[1] is not TextType.nonascii_byte_string) for left, (right_val, right_type) in zip(res, nonskipped): if right_type is TextType.unicode_string: assert left == right_val else: assert left.encode('ascii') == right_val def test_setup_logger(): with tempfile.NamedTemporaryFile('r') as tmpfile: with utils.AsyncLoggingHandler(tmpfile.name) as log_handler: logger = utils.setup_logger("test", utils.LogLevel.info, log_handler) logger.info("Hello World!") time.sleep(1) assert "Hello World!" in tmpfile.read() def test_setup_logger_overflow(): with tempfile.NamedTemporaryFile('r') as tmpfile: with utils.AsyncLoggingHandler(tmpfile.name, 1) as log_handler: logger = utils.setup_logger("test", utils.LogLevel.info, log_handler) for _ in range(10): logger.info("Hello World!") time.sleep(1) assert "Hello World!" in tmpfile.read() def test_setup_logger_stderr(capsys): with utils.AsyncLoggingHandler() as log_handler: logger = utils.setup_logger("test", utils.LogLevel.info, log_handler) logger.info("Hello World!") time.sleep(1) captured = capsys.readouterr() assert "Hello World!" in captured.err postfix-mta-sts-resolver-0.7.5/tests/testdata.py000066400000000000000000000005421361332105100217650ustar00rootroot00000000000000import os TESTDIR = os.path.dirname(os.path.realpath(__file__)) def load_testdata(dataset_name): filename = os.path.join(TESTDIR, dataset_name + '.tsv') with open(filename, 'rb') as f: for line in f: if not line: continue req, resp = line.rstrip(b'\r\n').split(b'\t') yield req, resp postfix-mta-sts-resolver-0.7.5/tests/tinyproxy.conf000066400000000000000000000237211361332105100225420ustar00rootroot00000000000000## ## tinyproxy.conf -- tinyproxy daemon configuration file ## ## This example tinyproxy.conf file contains example settings ## with explanations in comments. For decriptions of all ## parameters, see the tinproxy.conf(5) manual page. ## # # User/Group: This allows you to set the user and group that will be # used for tinyproxy after the initial binding to the port has been done # as the root user. Either the user or group name or the UID or GID # number may be used. # User nobody Group nogroup # # Port: Specify the port which tinyproxy will listen on. Please note # that should you choose to run on a port lower than 1024 you will need # to start tinyproxy using root. # Port 1380 # # Listen: If you have multiple interfaces this allows you to bind to # only one. If this is commented out, tinyproxy will bind to all # interfaces present. # Listen 127.0.0.2 # # Bind: This allows you to specify which interface will be used for # outgoing connections. This is useful for multi-home'd machines where # you want all traffic to appear outgoing from one particular interface. # #Bind 192.168.0.1 # # BindSame: If enabled, tinyproxy will bind the outgoing connection to the # ip address of the incoming connection. # #BindSame yes # # Timeout: The maximum number of seconds of inactivity a connection is # allowed to have before it is closed by tinyproxy. # Timeout 600 # # ErrorFile: Defines the HTML file to send when a given HTTP error # occurs. You will probably need to customize the location to your # particular install. The usual locations to check are: # /usr/local/share/tinyproxy # /usr/share/tinyproxy # /etc/tinyproxy # #ErrorFile 404 "/usr/share/tinyproxy/404.html" #ErrorFile 400 "/usr/share/tinyproxy/400.html" #ErrorFile 503 "/usr/share/tinyproxy/503.html" #ErrorFile 403 "/usr/share/tinyproxy/403.html" #ErrorFile 408 "/usr/share/tinyproxy/408.html" # # DefaultErrorFile: The HTML file that gets sent if there is no # HTML file defined with an ErrorFile keyword for the HTTP error # that has occured. # DefaultErrorFile "/usr/share/tinyproxy/default.html" # # StatHost: This configures the host name or IP address that is treated # as the stat host: Whenever a request for this host is received, # Tinyproxy will return an internal statistics page instead of # forwarding the request to that host. The default value of StatHost is # tinyproxy.stats. # #StatHost "tinyproxy.stats" # # # StatFile: The HTML file that gets sent when a request is made # for the stathost. If this file doesn't exist a basic page is # hardcoded in tinyproxy. # StatFile "/usr/share/tinyproxy/stats.html" # # Logfile: Allows you to specify the location where information should # be logged to. If you would prefer to log to syslog, then disable this # and enable the Syslog directive. These directives are mutually # exclusive. # Logfile "/var/log/tinyproxy/tinyproxy.log" # # Syslog: Tell tinyproxy to use syslog instead of a logfile. This # option must not be enabled if the Logfile directive is being used. # These two directives are mutually exclusive. # #Syslog On # # LogLevel: # # Set the logging level. Allowed settings are: # Critical (least verbose) # Error # Warning # Notice # Connect (to log connections without Info's noise) # Info (most verbose) # # The LogLevel logs from the set level and above. For example, if the # LogLevel was set to Warning, then all log messages from Warning to # Critical would be output, but Notice and below would be suppressed. # LogLevel Info # # PidFile: Write the PID of the main tinyproxy thread to this file so it # can be used for signalling purposes. # PidFile "/run/tinyproxy/tinyproxy.pid" # # XTinyproxy: Tell Tinyproxy to include the X-Tinyproxy header, which # contains the client's IP address. # #XTinyproxy Yes # # Upstream: # # Turns on upstream proxy support. # # The upstream rules allow you to selectively route upstream connections # based on the host/domain of the site being accessed. # # For example: # # connection to test domain goes through testproxy # upstream testproxy:8008 ".test.domain.invalid" # upstream testproxy:8008 ".our_testbed.example.com" # upstream testproxy:8008 "192.168.128.0/255.255.254.0" # # # no upstream proxy for internal websites and unqualified hosts # no upstream ".internal.example.com" # no upstream "www.example.com" # no upstream "10.0.0.0/8" # no upstream "192.168.0.0/255.255.254.0" # no upstream "." # # # connection to these boxes go through their DMZ firewalls # upstream cust1_firewall:8008 "testbed_for_cust1" # upstream cust2_firewall:8008 "testbed_for_cust2" # # # default upstream is internet firewall # upstream firewall.internal.example.com:80 # # The LAST matching rule wins the route decision. As you can see, you # can use a host, or a domain: # name matches host exactly # .name matches any host in domain "name" # . matches any host with no domain (in 'empty' domain) # IP/bits matches network/mask # IP/mask matches network/mask # #Upstream some.remote.proxy:port # # MaxClients: This is the absolute highest number of threads which will # be created. In other words, only MaxClients number of clients can be # connected at the same time. # MaxClients 10 # # MinSpareServers/MaxSpareServers: These settings set the upper and # lower limit for the number of spare servers which should be available. # # If the number of spare servers falls below MinSpareServers then new # server processes will be spawned. If the number of servers exceeds # MaxSpareServers then the extras will be killed off. # MinSpareServers 3 MaxSpareServers 10 # # StartServers: The number of servers to start initially. # StartServers 3 # # MaxRequestsPerChild: The number of connections a thread will handle # before it is killed. In practise this should be set to 0, which # disables thread reaping. If you do notice problems with memory # leakage, then set this to something like 10000. # MaxRequestsPerChild 0 # # Allow: Customization of authorization controls. If there are any # access control keywords then the default action is to DENY. Otherwise, # the default action is ALLOW. # # The order of the controls are important. All incoming connections are # tested against the controls based on order. # Allow 127.0.0.0/8 #Allow 192.168.0.0/16 #Allow 172.16.0.0/12 #Allow 10.0.0.0/8 # # AddHeader: Adds the specified headers to outgoing HTTP requests that # Tinyproxy makes. Note that this option will not work for HTTPS # traffic, as Tinyproxy has no control over what headers are exchanged. # #AddHeader "X-My-Header" "Powered by Tinyproxy" # # ViaProxyName: The "Via" header is required by the HTTP RFC, but using # the real host name is a security concern. If the following directive # is enabled, the string supplied will be used as the host name in the # Via header; otherwise, the server's host name will be used. # #ViaProxyName "tinyproxy" # # DisableViaHeader: When this is set to yes, Tinyproxy does NOT add # the Via header to the requests. This virtually puts Tinyproxy into # stealth mode. Note that RFC 2616 requires proxies to set the Via # header, so by enabling this option, you break compliance. # Don't disable the Via header unless you know what you are doing... # DisableViaHeader Yes # # Filter: This allows you to specify the location of the filter file. # #Filter "/etc/tinyproxy/filter" # # FilterURLs: Filter based on URLs rather than domains. # #FilterURLs On # # FilterExtended: Use POSIX Extended regular expressions rather than # basic. # #FilterExtended On # # FilterCaseSensitive: Use case sensitive regular expressions. # #FilterCaseSensitive On # # FilterDefaultDeny: Change the default policy of the filtering system. # If this directive is commented out, or is set to "No" then the default # policy is to allow everything which is not specifically denied by the # filter file. # # However, by setting this directive to "Yes" the default policy becomes # to deny everything which is _not_ specifically allowed by the filter # file. # #FilterDefaultDeny Yes # # Anonymous: If an Anonymous keyword is present, then anonymous proxying # is enabled. The headers listed are allowed through, while all others # are denied. If no Anonymous keyword is present, then all headers are # allowed through. You must include quotes around the headers. # # Most sites require cookies to be enabled for them to work correctly, so # you will need to allow Cookies through if you access those sites. # #Anonymous "Host" #Anonymous "Authorization" #Anonymous "Cookie" # # ConnectPort: This is a list of ports allowed by tinyproxy when the # CONNECT method is used. To disable the CONNECT method altogether, set # the value to 0. If no ConnectPort line is found, all ports are # allowed (which is not very secure.) # # The following two ports are used by SSL. # ConnectPort 443 ConnectPort 563 # # Configure one or more ReversePath directives to enable reverse proxy # support. With reverse proxying it's possible to make a number of # sites appear as if they were part of a single site. # # If you uncomment the following two directives and run tinyproxy # on your own computer at port 8888, you can access Google using # http://localhost:8888/google/ and Wired News using # http://localhost:8888/wired/news/. Neither will actually work # until you uncomment ReverseMagic as they use absolute linking. # #ReversePath "/google/" "http://www.google.com/" #ReversePath "/wired/" "http://www.wired.com/" # # When using tinyproxy as a reverse proxy, it is STRONGLY recommended # that the normal proxy is turned off by uncommenting the next directive. # #ReverseOnly Yes # # Use a cookie to track reverse proxy mappings. If you need to reverse # proxy sites which have absolute links you must uncomment this. # #ReverseMagic Yes # # The URL that's used to access this reverse proxy. The URL is used to # rewrite HTTP redirects so that they won't escape the proxy. If you # have a chain of reverse proxies, you'll need to put the outermost # URL here (the address which the end user types into his/her browser). # # If not set then no rewriting occurs. # #ReverseBaseURL "http://localhost:8888/" postfix-mta-sts-resolver-0.7.5/tox.ini000066400000000000000000000012231361332105100177500ustar00rootroot00000000000000[tox] envlist = py{35,36,37,38}{,-uvloop}, lint, cover skipsdist = true [testenv] passenv = TOXENV commands = py{35,36,37,38}: pip install -e '.[dev,sqlite,redis]' py{35,36,37,38}-uvloop: pip install -e '.[dev,sqlite,redis,uvloop]' pytest . [testenv:lint] basepython = python3.8 commands = pip install -e '.[dev,sqlite,redis]' pylint --reports=n --rcfile=.pylintrc postfix_mta_sts_resolver [testenv:cover] passenv = TOXENV basepython = python3.8 commands = pip install -e ".[dev,sqlite,redis]" pytest --cov . --cov-append --cov-report= . coverage report --fail-under=97 --include="postfix_mta_sts_resolver/*" --show-missing