├── certbot_dns_yandex ├── __init__.py ├── __main__.py ├── compat.py ├── plugin.py ├── cli.py └── client.py ├── setup.py ├── pyproject.toml ├── setup.cfg ├── README.md ├── .gitignore └── LICENSE /certbot_dns_yandex/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # filename: __init__.py 3 | -------------------------------------------------------------------------------- /certbot_dns_yandex/__main__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # filename: __main__.py 3 | 4 | if __name__ == '__main__': 5 | from .cli import main 6 | main() 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # filename: setup.py 3 | # 4 | # NOTE This file is used only for editable installations with 5 | # `pip install -e` command. 6 | 7 | if __name__ == '__main__': 8 | from setuptools import setup 9 | setup() 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # See PEP-517 and PEP-518 for details. 2 | 3 | [build-system] 4 | requires = ["setuptools", "setuptools_scm[toml]>=3.4", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [tool.setuptools_scm] 8 | write_to = "certbot_dns_yandex/version.py" 9 | -------------------------------------------------------------------------------- /certbot_dns_yandex/compat.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # filename: compat.py 3 | 4 | import sys 5 | 6 | 7 | if (3, 0) <= sys.version_info < (3, 9): 8 | def removesuffix(self, suffix): 9 | if self.endswith(suffix): 10 | return self[:-len(suffix)] 11 | return self 12 | else: 13 | removesuffix = str.removesuffix 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # See links below for documentaion notes. 2 | # 3 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/ 4 | # https://setuptools.readthedocs.io/en/latest/setuptools.html 5 | 6 | [metadata] 7 | name = certbot-dns-yandex 8 | url = https://github.com/daskol/certbot-dns-yandex 9 | description = Yandex DNS manager (aka PDD) authenticator plugin for Certbot 10 | long_description = file: README.md 11 | long_description_content_type = text/markdown 12 | platforms = Linux 13 | project_urls = 14 | Issues = https://github.com/daskol/certbot-dns-yandex 15 | Repository = https://github.com/daskol/certbot-dns-yandex 16 | Wiki = https://github.com/daskol/certbot-dns-yandex 17 | classifiers = 18 | Development Status :: 5 - Production/Stable 19 | Environment :: Console 20 | Environment :: Plugins 21 | Intended Audience :: System Administrators 22 | License :: Other/Proprietary License 23 | Natural Language :: English 24 | Operating System :: OS Independent 25 | Programming Language :: Python 26 | Programming Language :: Python :: 3 27 | Programming Language :: Python :: 3.8 28 | Programming Language :: Python :: 3.9 29 | Topic :: Internet :: WWW/HTTP 30 | Topic :: Security 31 | Topic :: System :: Installation/Setup 32 | Topic :: System :: Networking 33 | Topic :: System :: Systems Administration 34 | Topic :: Utilities 35 | Typing :: Typed 36 | 37 | [options] 38 | packages = find: 39 | python_requires = >=3.8,<4 40 | install_requires = 41 | acme>=1.10.0 42 | certbot>=1.10.0 43 | zope.interface 44 | 45 | [options.entry_points] 46 | certbot.plugins = 47 | dns-yandex = certbot_dns_yandex.plugin:Authenticator 48 | console_scripts = 49 | yandex-dns = certbot_dns_yandex.cli:main 50 | 51 | [options.packages.find] 52 | where=. 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certbot Plugin: Yandex DNS 2 | 3 | *Yandex DNS Authenticator plugin for Certbot* 4 | 5 | ## Overview 6 | 7 | Package provide certbot authenticator plugin for Yandex DNS (aka PDD) which 8 | allows to perform dns-01 probe. This kind of challange is the only way for now 9 | to issue wildcard certificates with certbot. 10 | 11 | NOTE This plugin will not be requested to merge in certbot repo until like 12 | issues [certbot/certbot][1][\#6464][6464], [certbot/certbot][1][\#6503][6503], 13 | and [certbot/certbot][1][\#6504][6504] are solved first. 14 | 15 | ### Installation 16 | 17 | The easies way to install plugin is insllation the plugin directly from git 18 | with PIP. 19 | 20 | ```bash 21 | pip install git+https://github.com/daskol/certbot-dns-yandex.git 22 | ``` 23 | 24 | ### Usage 25 | 26 | There is nothing special to use the plugin for certificate management. 27 | Essentially, the only things one should do is explicitely specify authenticator 28 | with option `-a` of certbot. Credentials to plugin could be passed either with 29 | CLI option `--dns-yandex-token` or environment variable `YANDEX_PDD_TOKEN`. 30 | The env variable has higher priority then CLI option. See example below. 31 | 32 | ```bash 33 | export YANDEX_PDD_TOKEN= 34 | certbot certonly -a dns-yandex --dns-yandex-token ... 35 | ``` 36 | 37 | ### CLI 38 | 39 | The package provides CLI `yandex-dns` for management DNS records in Yandex DNS 40 | from shell as well. As soon as the package installed one can list, add, or 41 | remove DNS records (several examples below). 42 | 43 | ```bash 44 | # List DNS records for a domain. 45 | yandex-dns ls example.org 46 | # Remove domain by DNS record ID for a domain. 47 | yandex-dns rm example.org 31513386 48 | # Add TXT record to DNS for a domain. 49 | yandex-dns add example.org TXT "Hello, world!" --subdomain greeting 50 | ``` 51 | 52 | [1]: https://github.com/certbot/certbot 53 | [6464]: https://github.com/certbot/certbot/issues/6464 54 | [6503]: https://github.com/certbot/certbot/issues/6503 55 | [6504]: https://github.com/certbot/certbot/issues/6504 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Autogenerated package version 132 | certbot_dns_yandex/version.py 133 | -------------------------------------------------------------------------------- /certbot_dns_yandex/plugin.py: -------------------------------------------------------------------------------- 1 | # encoing: utf8 2 | # filename: plugin.py 3 | 4 | import zope.interface 5 | 6 | from logging import getLogger 7 | 8 | from certbot.errors import PluginError 9 | from certbot.interfaces import IAuthenticator, IPluginFactory 10 | from certbot.plugins.dns_common import DNSAuthenticator 11 | 12 | from .client import Client 13 | from .compat import removesuffix 14 | 15 | 16 | logger = getLogger(__name__) 17 | 18 | 19 | @zope.interface.implementer(IAuthenticator) 20 | @zope.interface.provider(IPluginFactory) 21 | class Authenticator(DNSAuthenticator): 22 | """DNS Authenticator for Yandex DNS (aka PDD). 23 | 24 | This Authenticator uses the Yandex DNS API to fulfill a dns-01 challenge. 25 | """ 26 | 27 | description = 'Obtain certificates using a DNS TXT record (if you are ' \ 28 | 'using Yandex DNS manager aka PDD for DNS).' 29 | 30 | TTL = 60 # TTL for entry in DNS cache. 31 | 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | self.records_index = {} 35 | 36 | @classmethod 37 | def add_parser_arguments(cls, add): # pylint: disable=arguments-differ 38 | super().add_parser_arguments(add, default_propagation_seconds=60) 39 | add('token', 40 | default=None, 41 | help='Access token to API. Value could be specified via ' 42 | 'YANDEX_PDD_TOKEN environment variable.', 43 | type=str) 44 | 45 | def more_info(self) -> str: 46 | return 'This plugin configures a DNS TXT record to respond to a ' \ 47 | 'dns-01 challenge using the Yandex DNS API (aka PDD).' 48 | 49 | def _setup_credentials(self): 50 | # Just try to instantiate API bindings and check whether access token 51 | # exists. 52 | if Client(self.conf('token')).token is None: 53 | raise PluginError('Unable to get credentials (PDD access token). ' 54 | 'Automatic credentials lookup failed to get ' 55 | 'token from CLI optionsi and environment ' 56 | 'variable.') 57 | 58 | def _perform(self, domain, validation_name, validation): 59 | logger.debug('perform dns-01 probe for domain %s for record %s', 60 | domain, validation_name) 61 | 62 | kwargs = { 63 | 'ttl': Authenticator.TTL, 64 | 'subdomain': removesuffix(validation_name, domain)[:-1], 65 | } 66 | 67 | record = Client(self.conf('token')) \ 68 | .add(domain, 'TXT', validation, **kwargs) 69 | self.records_index[(domain, validation_name)] = record 70 | 71 | def _cleanup(self, domain, validation_name, validation): 72 | logger.debug('cleanup for domain %s for subdomain %s', 73 | domain, validation_name) 74 | 75 | if record := self.records_index.pop((domain, validation_name), None): 76 | Client(self.conf('token')).delete(domain, record.record_id) 77 | -------------------------------------------------------------------------------- /certbot_dns_yandex/cli.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # filename: cli.py 3 | """Simple CLI for viewing and modification of DNS records in Yandex DNS manager 4 | (Pochta-Dlya-Domena aka PDD). Acces token could be issued on the management 5 | page. Follow https://pddimp.yandex.ru/api2/admin/get_token to issue token. 6 | """ 7 | 8 | import logging 9 | import os 10 | 11 | from argparse import Action, ArgumentParser 12 | from dataclasses import asdict 13 | 14 | from .client import Client 15 | from .compat import removesuffix 16 | 17 | 18 | class EnvDefault(Action): 19 | 20 | def __init__(self, envvar, required=True, default=None, **kwargs): 21 | if not default and envvar: 22 | if envvar in os.environ: 23 | default = os.environ[envvar] 24 | if required and default: 25 | required = False 26 | super().__init__(default=default, required=required, **kwargs) 27 | 28 | def __call__(self, parser, namespace, values, option_string=None): 29 | setattr(namespace, self.dest, values) 30 | 31 | 32 | def add_domain(args): 33 | argnames = ( 34 | 'domain', 'type', 35 | 'content', 36 | 'priority', 37 | 'ttl', 38 | 'subdomain', 39 | 'admin_mail', 40 | 'weight', 41 | 'port', 42 | 'target', 43 | ) 44 | 45 | kwargs = {arg: getattr(args, arg) for arg in argnames} 46 | record = Client(args.token).add(**kwargs) 47 | fields = ('record_id', 'type', 'fqdn', 'content') 48 | 49 | for field in fields: 50 | value = getattr(record, field) 51 | name = field.replace('_', ' ') 52 | print(f'{name:>10s}: {value:<}') 53 | 54 | 55 | def del_domain(args): 56 | Client(args.token).delete(args.domain, args.record_id) 57 | 58 | 59 | def list_domains(args): 60 | domain = removesuffix(args.domain, '.') 61 | records = Client(args.token).list(domain) 62 | 63 | if not records: 64 | print('no dns records for', domain) 65 | return 66 | 67 | hdr = '{record_id:>12s} {type:>10s} {ttl:<6s} {fqdn:>24s} {content:28s}' 68 | fmt = '{record_id:>12d} {type:>10s} {ttl:<6d} {fqdn:>24s} {content:28s}' 69 | header = hdr.format(record_id='RECORD ID', 70 | type='TYPE', 71 | fqdn='DOMAIN', 72 | ttl='TTL', 73 | content='CONTENT') 74 | print(header) 75 | for record in records: 76 | if (fqdn := removesuffix(record.fqdn, domain)) == '': 77 | record.fqdn = '@' 78 | else: 79 | record.fqdn = removesuffix(fqdn, '.') 80 | print(fmt.format(**asdict(record))) 81 | 82 | 83 | parser = ArgumentParser(description=__doc__) 84 | parser.add_argument('--token', action=EnvDefault, envvar='YANDEX_PDD_TOKEN', type=str, help='access token') # noqa 85 | 86 | subparser = parser.add_subparsers(title='commands') 87 | 88 | domain_addition = subparser.add_parser('add') 89 | domain_addition.set_defaults(func=add_domain) 90 | domain_addition.add_argument('domain', type=str, help='domain of interest') 91 | domain_addition.add_argument('type', type=str, help='record type') 92 | domain_addition.add_argument('content', type=str, help='record value') 93 | domain_addition.add_argument('--subdomain', type=str, help='name of subdomain') 94 | domain_addition.add_argument('--ttl', type=int, help='record ttl') 95 | domain_addition.add_argument('--priority', type=int, help='record priority') 96 | domain_addition.add_argument('--admin-mail', type=int, help='email address of domain administrator') # noqa 97 | domain_addition.add_argument('--weight', type=int, help='weight of SRV record') 98 | domain_addition.add_argument('--port', type=int, help='service port in SRV record') # noqa 99 | domain_addition.add_argument('--target', type=str, help='canonical hostname of service in SRV record') # noqa 100 | 101 | domain_deletion = subparser.add_parser('rm') 102 | domain_deletion.set_defaults(func=del_domain) 103 | domain_deletion.add_argument('domain', type=str, help='domain of interest') 104 | domain_deletion.add_argument('record_id', type=int, help='identifier of DNS record') # noqa 105 | 106 | domain_listing = subparser.add_parser('ls') 107 | domain_listing.set_defaults(func=list_domains) 108 | domain_listing.add_argument('domain', type=str, help='domain of interest') 109 | 110 | 111 | def main(): 112 | logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', 113 | level=logging.INFO) 114 | 115 | args = parser.parse_args() 116 | if not hasattr(args, 'func'): 117 | subcommands = ', '.join(sorted(subparser.choices.keys())) 118 | parser.error(f'no subcommand (possible choices are {{{subcommands}}})') 119 | args.func(args) 120 | -------------------------------------------------------------------------------- /certbot_dns_yandex/client.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # filename: client.py 3 | 4 | from dataclasses import dataclass 5 | from http import HTTPStatus 6 | from http.client import HTTPResponse 7 | from json import load 8 | from os import getenv 9 | from typing import Any, Dict, List, Optional 10 | from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit 11 | from urllib.request import Request, urlopen 12 | 13 | 14 | @dataclass 15 | class Record: 16 | 17 | # Field record_id used by API for domain identification. 18 | record_id: int 19 | 20 | type: str 21 | domain: str 22 | fqdn: str 23 | content: str 24 | priority: int 25 | 26 | ttl: Optional[int] = None 27 | subdomain: Optional[str] = None 28 | 29 | 30 | class Client: 31 | """Class Client provides bindings to Yandex DSN Manager (PDD). 32 | See https://yandex.ru/dev/pdd/doc/concepts/api-dns.html for details. 33 | 34 | :param token: Access token (related envvar is YANDEX_PDD_TOKEN). 35 | """ 36 | 37 | ENDPOINT = 'https://pddimp.yandex.ru/api2/admin/dns/{method}' 38 | 39 | def __init__(self, token: Optional[str] = None): 40 | self.token = getenv('YANDEX_PDD_TOKEN') or token 41 | self.headers = {'PddToken': self.token} 42 | 43 | @staticmethod 44 | def _append_params(url: str, params: Dict[str, str]) -> str: 45 | scheme, netloc, path, query, fragment = urlsplit(url) 46 | values = parse_qs(query) 47 | values.update(params) 48 | query = urlencode(values) 49 | return urlunsplit((scheme, netloc, path, query, fragment)) 50 | 51 | @staticmethod 52 | def _make_query(params: Dict[str, Any]) -> bytes: 53 | # Assume callee collect params with locals() built-in function. 54 | params.pop('self') 55 | # Also, we ignore None values since thay are optional for the API. 56 | query = urlencode({k: v for k, v in params.items() if v is not None}) 57 | return query.encode() 58 | 59 | @staticmethod 60 | def _validate_response(res: HTTPResponse) -> Dict[str, Any]: 61 | if (code := res.getcode()) != HTTPStatus.OK: 62 | raise RuntimeError(f'Requets failed with status code {code}.') 63 | 64 | json = load(res) 65 | 66 | if (status := json.get('success')) == 'error': 67 | raise RuntimeError(f'Request failed: {json.get("error")}.') 68 | elif status != 'ok': 69 | raise RuntimeError(f'Unknown request status: {status}.') 70 | 71 | return json 72 | 73 | def _request(self, verb: str, method: str, 74 | params: Dict[str, Any]) -> Dict[str, Any]: 75 | """Method _request is a common routine which actually performs requests 76 | to Yandex DNS manager (PDD) API. 77 | 78 | :param verb: HTTP request verb. 79 | :param method: API method. 80 | :param params: Request params (either query string or form data). 81 | 82 | :return: Deserialized into dictionary JSON object. 83 | """ 84 | url = Client.ENDPOINT.format(method=method) 85 | if verb == 'GET': 86 | url = Client._append_params(url, params) 87 | data = None 88 | else: 89 | data = Client._make_query(params) 90 | req = Request(method=verb, url=url, headers=self.headers, data=data) 91 | res = urlopen(req) 92 | json = Client._validate_response(res) 93 | return json 94 | 95 | def add(self, domain: str, type: str, content: str, 96 | priority: Optional[int] = None, ttl: Optional[int] = None, 97 | subdomain: Optional[str] = None, admin_mail: Optional[str] = None, 98 | weight: Optional[int] = None, port: Optional[int] = None, 99 | target: Optional[str] = None,) -> Record: 100 | json = self._request('POST', 'add', locals()) 101 | return Record(**json.get('record', {})) 102 | 103 | def delete(self, domain: str, record_id: int) -> int: 104 | json = self._request('POST', 'del', locals()) 105 | return json.get('record_id') 106 | 107 | def edit(self, domain: str, record_id: int, 108 | admin_mail: Optional[str] = None, content: Optional[str] = None, 109 | expire: Optional[int] = None, neg_cache: Optional[int] = None, 110 | port: Optional[int] = None, priority: Optional[int] = None, 111 | refresh: Optional[int] = None, retry: Optional[int] = None, 112 | subdomain: Optional[str] = None, target: Optional[str] = None, 113 | ttl: Optional[int] = None, 114 | weight: Optional[int] = None) -> Record: 115 | json = self._request('POST', 'edit', locals()) 116 | return Record(**json.get('record', {})) 117 | 118 | def list(self, domain: str) -> List[Record]: 119 | json = self._request('GET', 'list', {'domain': domain}) 120 | return [Record(**entry) for entry in json.get('records', [])] 121 | 122 | 123 | __all__ = (Client, Record) 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------