├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── ci-cd.yml │ └── codeql.yml ├── .gitignore ├── .jscs.json ├── .jshintrc ├── .pep8rc ├── .pre-commit-config.yaml ├── .pylintrc ├── .pyup.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── aiohttp_cors ├── __about__.py ├── __init__.py ├── abc.py ├── cors_config.py ├── mixin.py ├── preflight_handler.py ├── py.typed ├── resource_options.py └── urldispatcher_router_adapter.py ├── install_python_and_pip.ps1 ├── pytest.ini ├── requirements-dev.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── doc ├── __init__.py └── test_basic_usage.py ├── integration ├── __init__.py ├── test_main.py ├── test_page.html └── test_real_browser.py └── unit ├── __init__.py ├── test___about__.py ├── test_cors_config.py ├── test_mixin.py ├── test_preflight_handler.py ├── test_resource_options.py └── test_urldispatcher_router_adapter.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --squash "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6 8 | tags: [ 'v*' ] 9 | pull_request: 10 | branches: 11 | - master 12 | - '[0-9].[0-9]+' 13 | 14 | 15 | jobs: 16 | lint: 17 | name: Linter 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Setup Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: 3.11 27 | cache: 'pip' 28 | cache-dependency-path: '**/requirements*.txt' 29 | - name: Pre-Commit hooks 30 | uses: pre-commit/action@v3.0.1 31 | - name: Install dependencies 32 | uses: py-actions/py-dependency-install@v4 33 | with: 34 | path: requirements-dev.txt 35 | - name: Install itself 36 | run: | 37 | pip install . 38 | - name: Run linter 39 | run: | 40 | make lint 41 | - name: Prepare twine checker 42 | run: | 43 | pip install -U build twine wheel 44 | python -m build 45 | - name: Run twine checker 46 | run: | 47 | twine check dist/* 48 | 49 | test: 50 | name: Test 51 | strategy: 52 | matrix: 53 | pyver: ['3.9', '3.10', '3.11', '3.12', '3.13'] 54 | runs-on: ubuntu-latest 55 | timeout-minutes: 15 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | - name: Setup Python ${{ matrix.pyver }} 60 | uses: actions/setup-python@v5 61 | with: 62 | python-version: ${{ matrix.pyver }} 63 | cache: 'pip' 64 | cache-dependency-path: '**/requirements*.txt' 65 | - name: Install dependencies 66 | uses: py-actions/py-dependency-install@v4 67 | with: 68 | path: requirements-dev.txt 69 | - name: Run unittests 70 | run: pytest tests 71 | env: 72 | COLOR: 'yes' 73 | # - run: python -m coverage xml 74 | # - name: Upload coverage 75 | # uses: codecov/codecov-action@v5 76 | # with: 77 | # fail_ci_if_error: true 78 | # token: ${{ secrets.CODECOV_TOKEN }} 79 | 80 | check: # This job does nothing and is only used for the branch protection 81 | if: always() 82 | 83 | needs: [lint, test] 84 | 85 | runs-on: ubuntu-latest 86 | 87 | steps: 88 | - name: Decide whether the needed jobs succeeded or failed 89 | uses: re-actors/alls-green@release/v1 90 | with: 91 | jobs: ${{ toJSON(needs) }} 92 | 93 | deploy: 94 | name: Deploy 95 | runs-on: ubuntu-latest 96 | needs: [check] 97 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 98 | permissions: 99 | contents: write # IMPORTANT: mandatory for making GitHub Releases 100 | id-token: write # IMPORTANT: mandatory for trusted publishing & sigstore 101 | environment: 102 | name: pypi 103 | url: >- 104 | https://pypi.org/project/${{ env.PROJECT_NAME }}/${{ github.ref_name }} 105 | steps: 106 | - name: Checkout 107 | uses: actions/checkout@v4 108 | - name: Setup Python 109 | uses: actions/setup-python@v5 110 | with: 111 | python-version: 3.9 112 | - name: Install dependencies 113 | run: 114 | python -m pip install -U pip wheel setuptools build twine 115 | - name: Build dists 116 | run: | 117 | python -m build 118 | - name: >- 119 | Publish 🐍📦 to PyPI 120 | uses: pypa/gh-action-pypi-publish@release/v1 121 | - name: Sign the dists with Sigstore 122 | uses: sigstore/gh-action-sigstore-python@v3.0.0 123 | with: 124 | inputs: >- 125 | ./dist/${{ env.PROJECT_NAME }}*.tar.gz 126 | ./dist/${{ env.PROJECT_NAME }}*.whl 127 | - name: Upload artifact signatures to GitHub Release 128 | # Confusingly, this action also supports updating releases, not 129 | # just creating them. This is what we want here, since we've manually 130 | # created the release above. 131 | uses: softprops/action-gh-release@v2 132 | with: 133 | # dist/ contains the built packages, which smoketest-artifacts/ 134 | # contains the signatures and certificates. 135 | files: dist/** 136 | # - name: Make Release 137 | # uses: aio-libs/create-release@v1.6.6 138 | # with: 139 | # changes_file: CHANGES.rst 140 | # name: aiohttp-jinja2 141 | # version_file: aiohttp_jinja2/__init__.py 142 | # github_token: ${{ secrets.GITHUB_TOKEN }} 143 | # pypi_token: ${{ secrets.PYPI_API_TOKEN }} 144 | # dist_dir: dist 145 | # fix_issue_regex: "`#(\\d+) `" 146 | # fix_issue_repl: "(#\\1)" 147 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "43 0 * * 4" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript, python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | if: ${{ matrix.language == 'javascript' || matrix.language == 'python' }} 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: "/language:${{ matrix.language }}" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Usually these files are written by a python script from a template 2 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 3 | # Byte-compiled / optimized / DLL files 4 | # C extensions 5 | # Distribution / packaging 6 | # Installer logs 7 | # PyBuilder 8 | # PyInstaller 9 | # Sphinx documentation 10 | # Unit test / coverage reports 11 | *,cover 12 | *.egg-info/ 13 | *.manifest 14 | *.py[cod] 15 | *.so 16 | *.spec 17 | .Python 18 | .coverage 19 | .coverage.* 20 | .eggs/ 21 | .installed.cfg 22 | .pytest_cache 23 | /*.egg 24 | /.cache 25 | /.tox/ 26 | /build/ 27 | /dist/ 28 | /env/ 29 | __pycache__/ 30 | coverage.xml 31 | develop-eggs/ 32 | docs/_build/ 33 | downloads/ 34 | eggs/ 35 | geckodriver.log 36 | htmlcov/ 37 | lib/ 38 | lib64/ 39 | nosetests.xml 40 | parts/ 41 | pip-delete-this-directory.txt 42 | pip-log.txt 43 | sdist/ 44 | target/ 45 | var/ 46 | .python-version 47 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireOperatorBeforeLineBreak": true, 12 | "requireCamelCaseOrUpperCaseIdentifiers": true, 13 | "maximumLineLength": { 14 | "value": 80, 15 | "allowComments": true, 16 | "allowRegex": true 17 | }, 18 | "validateIndentation": 2, 19 | "validateQuoteMarks": "'", 20 | 21 | "disallowMultipleLineStrings": true, 22 | "disallowMixedSpacesAndTabs": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowSpaceAfterPrefixUnaryOperators": true, 25 | "disallowMultipleVarDecl": true, 26 | 27 | "requireSpaceAfterKeywords": [ 28 | "if", 29 | "else", 30 | "for", 31 | "while", 32 | "do", 33 | "switch", 34 | "return", 35 | "try", 36 | "catch" 37 | ], 38 | "requireSpaceBeforeBinaryOperators": [ 39 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 40 | "&=", "|=", "^=", "+=", 41 | 42 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 43 | "|", "^", "&&", "||", "===", "==", ">=", 44 | "<=", "<", ">", "!=", "!==" 45 | ], 46 | "requireSpaceAfterBinaryOperators": true, 47 | "requireSpacesInConditionalExpression": true, 48 | "requireSpaceBeforeBlockStatements": true, 49 | "requireSpacesInForStatement": true, 50 | "requireLineFeedAtFileEnd": true, 51 | "requireSpacesInFunctionExpression": { 52 | "beforeOpeningCurlyBrace": true 53 | }, 54 | "disallowSpacesInsideObjectBrackets": "all", 55 | "disallowSpacesInsideArrayBrackets": "all", 56 | "disallowSpacesInsideParentheses": true, 57 | 58 | "disallowMultipleLineBreaks": true, 59 | "disallowNewlineBeforeBlockStatements": [ 60 | "if", "else", "try", "catch", "finally", "do", "while", "for", "function" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "bitwise": false, 4 | "boss": false, 5 | "browser": true, 6 | "camelcase": true, 7 | "couch": false, 8 | "curly": true, 9 | "debug": false, 10 | "devel": true, 11 | "dojo": false, 12 | "eqeqeq": true, 13 | "eqnull": true, 14 | "es3": true, 15 | "evil": false, 16 | "expr": true, 17 | "forin": false, 18 | "funcscope": true, 19 | "globalstrict": false, 20 | "immed": true, 21 | "iterator": false, 22 | "jquery": false, 23 | "lastsemic": false, 24 | "latedef": false, 25 | "laxbreak": true, 26 | "laxcomma": false, 27 | "loopfunc": true, 28 | "mootools": false, 29 | "multistr": false, 30 | "newcap": true, 31 | "noarg": true, 32 | "node": false, 33 | "noempty": false, 34 | "nonew": true, 35 | "nonstandard": false, 36 | "nomen": false, 37 | "onecase": false, 38 | "onevar": false, 39 | "passfail": false, 40 | "plusplus": false, 41 | "proto": false, 42 | "prototypejs": false, 43 | "regexdash": true, 44 | "regexp": false, 45 | "rhino": false, 46 | "undef": true, 47 | "unused": "strict", 48 | "scripturl": true, 49 | "shadow": false, 50 | "smarttabs": true, 51 | "strict": false, 52 | "sub": false, 53 | "supernew": false, 54 | "trailing": true, 55 | "validthis": true, 56 | "withstmt": false, 57 | "white": true, 58 | "worker": false, 59 | "wsh": false, 60 | "yui": false, 61 | "indent": 4, 62 | "predef": ["require", "define", "JSON"], 63 | "quotmark": "single", 64 | "maxcomplexity": 10, 65 | "esnext": true 66 | } 67 | -------------------------------------------------------------------------------- /.pep8rc: -------------------------------------------------------------------------------- 1 | ; TODO: This configuration currently not used in pytest-pep8, see 2 | ; 3 | 4 | [pep8] 5 | show-source = yes 6 | statistics = yes 7 | count = yes 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: 'v5.0.0' 4 | hooks: 5 | - id: check-merge-conflict 6 | exclude: "rst$" 7 | - repo: https://github.com/asottile/yesqa 8 | rev: v1.5.0 9 | hooks: 10 | - id: yesqa 11 | - repo: https://github.com/PyCQA/isort 12 | rev: '6.0.1' 13 | hooks: 14 | - id: isort 15 | - repo: https://github.com/psf/black 16 | rev: '25.1.0' 17 | hooks: 18 | - id: black 19 | language_version: python3 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: 'v5.0.0' 22 | hooks: 23 | - id: check-case-conflict 24 | - id: check-json 25 | - id: check-xml 26 | - id: check-yaml 27 | - id: debug-statements 28 | - id: check-added-large-files 29 | - id: end-of-file-fixer 30 | exclude: "[.]md$" 31 | - id: requirements-txt-fixer 32 | - id: trailing-whitespace 33 | exclude: "[.]ref$" 34 | - id: check-symlinks 35 | - id: debug-statements 36 | - repo: https://github.com/asottile/pyupgrade 37 | rev: 'v3.19.1' 38 | hooks: 39 | - id: pyupgrade 40 | args: ['--py39-plus'] 41 | - repo: https://github.com/PyCQA/flake8 42 | rev: '7.1.2' 43 | hooks: 44 | - id: flake8 45 | - repo: https://github.com/rhysd/actionlint 46 | rev: v1.7.7 47 | hooks: 48 | - id: actionlint-docker 49 | args: 50 | - -ignore 51 | - 'SC2155:' 52 | - -ignore 53 | - 'SC2086:' 54 | - -ignore 55 | - 'SC1004:' 56 | ci: 57 | skip: 58 | - actionlint-docker 59 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore= 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | # Deprecated. It was used to include message's id in output. Use --msg-template 25 | # instead. 26 | #include-ids=no 27 | 28 | # Deprecated. It was used to include symbolic ids of messages in output. Use 29 | # --msg-template instead. 30 | #symbols=no 31 | 32 | # Use multiple processes to speed up Pylint. 33 | jobs=1 34 | 35 | # Allow loading of arbitrary C extensions. Extensions are imported into the 36 | # active Python interpreter and may run arbitrary code. 37 | unsafe-load-any-extension=no 38 | 39 | # A comma-separated list of package or module names from where C extensions may 40 | # be loaded. Extensions are loading into the active Python interpreter and may 41 | # run arbitrary code 42 | extension-pkg-whitelist= 43 | 44 | # Allow optimization of some AST trees. This will activate a peephole AST 45 | # optimizer, which will apply various small optimizations. For instance, it can 46 | # be used to obtain the result of joining multiple strings with the addition 47 | # operator. Joining a lot of strings can lead to a maximum recursion error in 48 | # Pylint and this flag can prevent that. It has one side effect, the resulting 49 | # AST will be different than the one from reality. 50 | optimize-ast=no 51 | 52 | 53 | [MESSAGES CONTROL] 54 | 55 | # Only show warnings with the listed confidence levels. Leave empty to show 56 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 57 | confidence= 58 | 59 | # Enable the message, report, category or checker with the given id(s). You can 60 | # either give multiple identifier separated by comma (,) or put this option 61 | # multiple time. See also the "--disable" option for examples. 62 | #enable= 63 | 64 | # Disable the message, report, category or checker with the given id(s). You 65 | # can either give multiple identifiers separated by comma (,) or put this 66 | # option multiple times (only on the command line, not in the configuration 67 | # file where it should appear only once).You can also use "--disable=all" to 68 | # disable everything first and then reenable specific checks. For example, if 69 | # you want to run only the similarities checker, you can use "--disable=all 70 | # --enable=similarities". If you want to run only the classes checker, but have 71 | # no Warning level messages displayed, use"--disable=all --enable=classes 72 | # --disable=W" 73 | disable=E1604,W1629,W1605,I0020,W1609,W1615,W1610,W1618,W1608,W1622,W1640,E1603,W1635,W1636,W1634,W1628,W1614,E1601,W1601,I0021,E1605,W1611,W1612,W1619,W1616,W1638,W1626,W1630,W1607,E1602,W1623,W1613,W1606,W1625,W0704,W1639,W1603,W1632,E1606,W1602,W1637,W1624,W1620,E1608,W1627,E1607,W1633,W1604,W1617,W1621 74 | 75 | 76 | [REPORTS] 77 | 78 | # Set the output format. Available formats are text, parseable, colorized, msvs 79 | # (visual studio) and html. You can also give a reporter class, eg 80 | # mypackage.mymodule.MyReporterClass. 81 | output-format=text 82 | 83 | # Put messages in a separate file for each module / package specified on the 84 | # command line instead of printing them on stdout. Reports (if any) will be 85 | # written in a file name "pylint_global.[txt|html]". 86 | files-output=no 87 | 88 | # Tells whether to display a full report or only the messages 89 | reports=yes 90 | 91 | # Python expression which should return a note less than 10 (10 is the highest 92 | # note). You have access to the variables errors warning, statement which 93 | # respectively contain the number of errors / warnings messages and the total 94 | # number of statements analyzed. This is used by the global evaluation report 95 | # (RP0004). 96 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 97 | 98 | # Add a comment according to your evaluation note. This is used by the global 99 | # evaluation report (RP0004). 100 | comment=no 101 | 102 | # Template used to display messages. This is a python new-style format string 103 | # used to format the message information. See doc for all details. 104 | # Path/line on separate added to allow IDE to parse error messages and 105 | # provide interactive link on the place of the error. 106 | msg-template={path}:{line}: 107 | {C}:{msg_id}:{line:3d},{column:2d}: {msg} ({symbol}) 108 | 109 | 110 | [LOGGING] 111 | 112 | # Logging modules to check that the string format arguments are in logging 113 | # function parameter format 114 | logging-modules=logging 115 | 116 | 117 | [FORMAT] 118 | 119 | # Maximum number of characters on a single line. 120 | max-line-length=100 121 | 122 | # Regexp for a line that is allowed to be longer than the limit. 123 | ignore-long-lines=^\s*(# )??$ 124 | 125 | # Allow the body of an if to be on the same line as the test if there is no 126 | # else. 127 | single-line-if-stmt=no 128 | 129 | # List of optional constructs for which whitespace checking is disabled 130 | no-space-check=trailing-comma,dict-separator 131 | 132 | # Maximum number of lines in a module 133 | max-module-lines=1000 134 | 135 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 136 | # tab). 137 | indent-string=' ' 138 | 139 | # Number of spaces of indent required inside a hanging or continued line. 140 | indent-after-paren=4 141 | 142 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 143 | expected-line-ending-format= 144 | 145 | 146 | [MISCELLANEOUS] 147 | 148 | # List of note tags to take in consideration, separated by a comma. 149 | notes=FIXME,XXX,TODO 150 | 151 | 152 | [VARIABLES] 153 | 154 | # Tells whether we should check for unused import in __init__ files. 155 | init-import=no 156 | 157 | # A regular expression matching the name of dummy variables (i.e. expectedly 158 | # not used). 159 | dummy-variables-rgx=_$|dummy 160 | 161 | # List of additional names supposed to be defined in builtins. Remember that 162 | # you should avoid to define new builtins when possible. 163 | additional-builtins= 164 | 165 | # List of strings which can identify a callback function by name. A callback 166 | # name must start or end with one of those strings. 167 | callbacks=cb_,_cb 168 | 169 | 170 | [BASIC] 171 | 172 | # Required attributes for module, separated by a comma 173 | required-attributes= 174 | 175 | # List of builtins function names that should not be used, separated by a comma 176 | bad-functions=map,filter 177 | 178 | # Good variable names which should always be accepted, separated by a comma 179 | good-names=i,j,k,ex,Run,_ 180 | 181 | # Bad variable names which should always be refused, separated by a comma 182 | bad-names=foo,bar,baz,toto,tutu,tata 183 | 184 | # Colon-delimited sets of names that determine each other's naming style when 185 | # the name regexes allow several styles. 186 | name-group= 187 | 188 | # Include a hint for the correct naming format with invalid-name 189 | include-naming-hint=no 190 | 191 | # Regular expression matching correct module names 192 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 193 | 194 | # Naming hint for module names 195 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 196 | 197 | # Regular expression matching correct method names 198 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 199 | 200 | # Naming hint for method names 201 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 202 | 203 | # Regular expression matching correct class attribute names 204 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 205 | 206 | # Naming hint for class attribute names 207 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 208 | 209 | # Regular expression matching correct class names 210 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 211 | 212 | # Naming hint for class names 213 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 214 | 215 | # Regular expression matching correct function names 216 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 217 | 218 | # Naming hint for function names 219 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 220 | 221 | # Regular expression matching correct inline iteration names 222 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 223 | 224 | # Naming hint for inline iteration names 225 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 226 | 227 | # Regular expression matching correct constant names 228 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 229 | 230 | # Naming hint for constant names 231 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 232 | 233 | # Regular expression matching correct argument names 234 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 235 | 236 | # Naming hint for argument names 237 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 238 | 239 | # Regular expression matching correct attribute names 240 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 241 | 242 | # Naming hint for attribute names 243 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 244 | 245 | # Regular expression matching correct variable names 246 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 247 | 248 | # Naming hint for variable names 249 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 250 | 251 | # Regular expression which should only match function or class names that do 252 | # not require a docstring. 253 | no-docstring-rgx=__.*__ 254 | 255 | # Minimum line length for functions/classes that require docstrings, shorter 256 | # ones are exempt. 257 | docstring-min-length=-1 258 | 259 | 260 | [TYPECHECK] 261 | 262 | # Tells whether missing members accessed in mixin class should be ignored. A 263 | # mixin class is detected if its name ends with "mixin" (case insensitive). 264 | ignore-mixin-members=yes 265 | 266 | # List of module names for which member attributes should not be checked 267 | # (useful for modules/projects where namespaces are manipulated during runtime 268 | # and thus existing member attributes cannot be deduced by static analysis 269 | ignored-modules= 270 | 271 | # List of classes names for which member attributes should not be checked 272 | # (useful for classes with attributes dynamically set). 273 | ignored-classes=SQLObject 274 | 275 | # When zope mode is activated, add a predefined set of Zope acquired attributes 276 | # to generated-members. 277 | zope=no 278 | 279 | # List of members which are set dynamically and missed by pylint inference 280 | # system, and so shouldn't trigger E0201 when accessed. Python regular 281 | # expressions are accepted. 282 | generated-members=REQUEST,acl_users,aq_parent 283 | 284 | 285 | [SPELLING] 286 | 287 | # Spelling dictionary name. Available dictionaries: none. To make it working 288 | # install python-enchant package. 289 | spelling-dict= 290 | 291 | # List of comma separated words that should not be checked. 292 | spelling-ignore-words= 293 | 294 | # A path to a file that contains private dictionary; one word per line. 295 | spelling-private-dict-file= 296 | 297 | # Tells whether to store unknown words to indicated private dictionary in 298 | # --spelling-private-dict-file option instead of raising a message. 299 | spelling-store-unknown-words=no 300 | 301 | 302 | [SIMILARITIES] 303 | 304 | # Minimum lines number of a similarity. 305 | min-similarity-lines=4 306 | 307 | # Ignore comments when computing similarities. 308 | ignore-comments=yes 309 | 310 | # Ignore docstrings when computing similarities. 311 | ignore-docstrings=yes 312 | 313 | # Ignore imports when computing similarities. 314 | ignore-imports=no 315 | 316 | 317 | [IMPORTS] 318 | 319 | # Deprecated modules which should not be used, separated by a comma 320 | deprecated-modules=stringprep,optparse 321 | 322 | # Create a graph of every (i.e. internal and external) dependencies in the 323 | # given file (report RP0402 must not be disabled) 324 | import-graph= 325 | 326 | # Create a graph of external dependencies in the given file (report RP0402 must 327 | # not be disabled) 328 | ext-import-graph= 329 | 330 | # Create a graph of internal dependencies in the given file (report RP0402 must 331 | # not be disabled) 332 | int-import-graph= 333 | 334 | 335 | [CLASSES] 336 | 337 | # List of interface methods to ignore, separated by a comma. This is used for 338 | # instance to not check methods defines in Zope's Interface base class. 339 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 340 | 341 | # List of method names used to declare (i.e. assign) instance attributes. 342 | defining-attr-methods=__init__,__new__,setUp 343 | 344 | # List of valid names for the first argument in a class method. 345 | valid-classmethod-first-arg=cls 346 | 347 | # List of valid names for the first argument in a metaclass class method. 348 | valid-metaclass-classmethod-first-arg=mcs 349 | 350 | # List of member names, which should be excluded from the protected access 351 | # warning. 352 | exclude-protected=_asdict,_fields,_replace,_source,_make 353 | 354 | 355 | [DESIGN] 356 | 357 | # Maximum number of arguments for function / method 358 | max-args=5 359 | 360 | # Argument names that match this expression will be ignored. Default to name 361 | # with leading underscore 362 | ignored-argument-names=_.* 363 | 364 | # Maximum number of locals for function / method body 365 | max-locals=15 366 | 367 | # Maximum number of return / yield for function / method body 368 | max-returns=6 369 | 370 | # Maximum number of branch for function / method body 371 | max-branches=12 372 | 373 | # Maximum number of statements in function / method body 374 | max-statements=50 375 | 376 | # Maximum number of parents for a class (see R0901). 377 | max-parents=7 378 | 379 | # Maximum number of attributes for a class (see R0902). 380 | max-attributes=7 381 | 382 | # Minimum number of public methods for a class (see R0903). 383 | min-public-methods=2 384 | 385 | # Maximum number of public methods for a class (see R0904). 386 | max-public-methods=20 387 | 388 | 389 | [EXCEPTIONS] 390 | 391 | # Exceptions that will emit a warning when being caught. Defaults to 392 | # "Exception" 393 | overgeneral-exceptions=Exception 394 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # Label PRs with `deps-update` label 2 | label_prs: deps-update 3 | 4 | schedule: every week 5 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | CHANGES 3 | ========= 4 | 5 | 0.8.1 (2025-03-31) 6 | ================== 7 | 8 | - Fix packaging to not install on Python 3.8. 9 | 10 | 0.8.0 (2025-03-11) 11 | ================== 12 | 13 | - Make the library compatible with aiohttp 3.9+ and Python 3.9+ 14 | 15 | 0.7.0 (2018-03-05) 16 | ================== 17 | 18 | - Make web view check implicit and type based (#159) 19 | 20 | - Disable Python 3.4 support (#156) 21 | 22 | - Support aiohttp 3.0+ (#155) 23 | 24 | 0.6.0 (2017-12-21) 25 | ================== 26 | 27 | - Support aiohttp views by ``CorsViewMixin`` (#145) 28 | 29 | 0.5.3 (2017-04-21) 30 | ================== 31 | 32 | - Fix ``typing`` being installed on Python 3.6. 33 | 34 | 0.5.2 (2017-03-28) 35 | ================== 36 | 37 | - Fix tests compatibility with ``aiohttp`` 2.0. 38 | This release and release v0.5.0 should work on ``aiohttp`` 2.0. 39 | 40 | 41 | 0.5.1 (2017-03-23) 42 | ================== 43 | 44 | - Enforce ``aiohttp`` version to be less than 2.0. 45 | Newer ``aiohttp`` releases will be supported in the next release. 46 | 47 | 0.5.0 (2016-11-18) 48 | ================== 49 | 50 | - Fix compatibility with ``aiohttp`` 1.1 51 | 52 | 53 | 0.4.0 (2016-04-04) 54 | ================== 55 | 56 | - Fixed support with new Resources objects introduced in ``aiohttp`` 0.21.0. 57 | Minimum supported version of ``aiohttp`` is 0.21.4 now. 58 | 59 | - New Resources objects are supported. 60 | You can specify default configuration for a Resource and use 61 | ``allow_methods`` to explicitly list allowed methods (or ``*`` for all 62 | HTTP methods): 63 | 64 | .. code-block:: python 65 | 66 | # Allow POST and PUT requests from "http://client.example.org" origin. 67 | hello_resource = cors.add(app.router.add_resource("/hello"), { 68 | "http://client.example.org": 69 | aiohttp_cors.ResourceOptions( 70 | allow_methods=["POST", "PUT"]), 71 | }) 72 | # No need to add POST and PUT routes into CORS configuration object. 73 | hello_resource.add_route("POST", handler_post) 74 | hello_resource.add_route("PUT", handler_put) 75 | # Still you can add additional methods to CORS configuration object: 76 | cors.add(hello_resource.add_route("DELETE", handler_delete)) 77 | 78 | - ``AbstractRouterAdapter`` was completely rewritten to be more Router 79 | agnostic. 80 | 81 | 0.3.0 (2016-02-06) 82 | ================== 83 | 84 | - Rename ``UrlDistatcherRouterAdapter`` to ``UrlDispatcherRouterAdapter``. 85 | 86 | - Set maximum supported ``aiohttp`` version to ``0.20.2``, see bug #30 for 87 | details. 88 | 89 | 0.2.0 (2015-11-30) 90 | ================== 91 | 92 | - Move ABCs from ``aiohttp_cors.router_adapter`` to ``aiohttp_cors.abc``. 93 | 94 | - Rename ``RouterAdapter`` to ``AbstractRouterAdapter``. 95 | 96 | - Fix bug with configuring CORS for named routes. 97 | 98 | 0.1.0 (2015-11-05) 99 | ================== 100 | 101 | * Initial release. 102 | -------------------------------------------------------------------------------- /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 2015-2018 Vladimir Rutsky and aio-libs team 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGES.rst 3 | include README.rst 4 | recursive-include tests *.py 5 | include tests/integration/test_page.html 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | 4 | lint: 5 | pre-commit run --all-files 6 | 7 | test: lint 8 | pytest tests 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | CORS support for aiohttp 3 | ======================== 4 | 5 | ``aiohttp_cors`` library implements 6 | `Cross Origin Resource Sharing (CORS) `__ 7 | support for `aiohttp `__ 8 | asyncio-powered asynchronous HTTP server. 9 | 10 | Jump directly to `Usage`_ part to see how to use ``aiohttp_cors``. 11 | 12 | Same-origin policy 13 | ================== 14 | 15 | Web security model is tightly connected to 16 | `Same-origin policy (SOP) `__. 17 | In short: web pages cannot *Read* resources which origin 18 | doesn't match origin of requested page, but can *Embed* (or *Execute*) 19 | resources and have limited ability to *Write* resources. 20 | 21 | Origin of a page is defined in the `Standard `__ as tuple 22 | ``(schema, host, port)`` 23 | (there is a notable exception with Internet Explorer: it doesn't use port to 24 | define origin, but uses it's own 25 | `Security Zones `__). 26 | 27 | Can *Embed* means that resource from other origin can be embedded into 28 | the page, 29 | e.g. by using `` 472 | 473 | 474 | -------------------------------------------------------------------------------- /tests/integration/test_real_browser.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Vladimir Rutsky 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """System test using real browser.""" 16 | 17 | import asyncio 18 | import json 19 | import logging 20 | import os 21 | import pathlib 22 | import socket 23 | import webbrowser 24 | 25 | import pytest 26 | import selenium.common.exceptions 27 | from selenium import webdriver 28 | from selenium.webdriver.common.by import By 29 | from selenium.webdriver.common.keys import Keys 30 | from selenium.webdriver.support import expected_conditions as EC 31 | from selenium.webdriver.support.ui import WebDriverWait 32 | 33 | from aiohttp import hdrs, web 34 | from aiohttp_cors import CorsViewMixin, ResourceOptions, setup as _setup 35 | 36 | 37 | class _ServerDescr: 38 | """Auxiliary class for storing server info""" 39 | 40 | def __init__(self): 41 | self.app = None 42 | self.cors = None 43 | self.handler = None 44 | self.server = None 45 | self.url = None 46 | 47 | 48 | class IntegrationServers: 49 | """Integration servers starting/stopping manager""" 50 | 51 | def __init__(self, use_resources, use_webview, *, loop=None): 52 | self.servers = {} 53 | 54 | self.loop = loop 55 | if self.loop is None: 56 | self.loop = asyncio.get_event_loop() 57 | 58 | self.use_resources = use_resources 59 | self.use_webview = use_webview 60 | 61 | self._logger = logging.getLogger("IntegrationServers") 62 | 63 | @property 64 | def origin_server_url(self): 65 | return self.servers["origin"].url 66 | 67 | async def start_servers(self): 68 | test_page_path = pathlib.Path(__file__).with_name("test_page.html") 69 | 70 | async def handle_test_page(request: web.Request) -> web.StreamResponse: 71 | with test_page_path.open("r", encoding="utf-8") as f: 72 | return web.Response( 73 | text=f.read(), headers={hdrs.CONTENT_TYPE: "text/html"} 74 | ) 75 | 76 | async def handle_no_cors(request: web.Request) -> web.StreamResponse: 77 | return web.Response( 78 | text="""{"type": "no_cors.json"}""", 79 | headers={hdrs.CONTENT_TYPE: "application/json"}, 80 | ) 81 | 82 | async def handle_resource(request: web.Request) -> web.StreamResponse: 83 | return web.Response( 84 | text="""{"type": "resource"}""", 85 | headers={hdrs.CONTENT_TYPE: "application/json"}, 86 | ) 87 | 88 | async def handle_servers_addresses(request: web.Request) -> web.StreamResponse: 89 | servers_addresses = { 90 | name: descr.url for name, descr in self.servers.items() 91 | } 92 | return web.Response(text=json.dumps(servers_addresses)) 93 | 94 | class ResourceView(web.View, CorsViewMixin): 95 | 96 | async def get(self) -> web.StreamResponse: 97 | return await handle_resource(self.request) 98 | 99 | # For most resources: 100 | # "origin" server has no CORS configuration. 101 | # "allowing" server explicitly allows CORS requests to "origin" server. 102 | # "denying" server explicitly disallows CORS requests to "origin" 103 | # server. 104 | # "free_for_all" server allows CORS requests for all origins server. 105 | # "no_cors" server has no CORS configuration. 106 | cors_server_names = ["allowing", "denying", "free_for_all"] 107 | server_names = cors_server_names + ["origin", "no_cors"] 108 | 109 | for server_name in server_names: 110 | assert server_name not in self.servers 111 | self.servers[server_name] = _ServerDescr() 112 | 113 | server_sockets = {} 114 | 115 | # Create applications and sockets. 116 | for server_name, server_descr in self.servers.items(): 117 | server_descr.app = web.Application() 118 | 119 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 120 | sock.bind(("127.0.0.1", 0)) 121 | sock.listen(10) 122 | server_sockets[server_name] = sock 123 | 124 | hostaddr, port = sock.getsockname() 125 | server_descr.url = f"http://{hostaddr}:{port}" 126 | 127 | # Server test page from origin server. 128 | self.servers["origin"].app.router.add_route("GET", "/", handle_test_page) 129 | self.servers["origin"].app.router.add_route( 130 | "GET", "/servers_addresses", handle_servers_addresses 131 | ) 132 | 133 | # Add routes to all servers. 134 | for server_name in server_names: 135 | app = self.servers[server_name].app 136 | app.router.add_route("GET", "/no_cors.json", handle_no_cors) 137 | if self.use_webview: 138 | app.router.add_route( 139 | "*", "/cors_resource", ResourceView, name="cors_resource" 140 | ) 141 | else: 142 | app.router.add_route( 143 | "GET", "/cors_resource", handle_resource, name="cors_resource" 144 | ) 145 | 146 | cors_default_configs = { 147 | "allowing": { 148 | self.servers["origin"].url: ResourceOptions( 149 | allow_credentials=True, expose_headers="*", allow_headers="*" 150 | ) 151 | }, 152 | "denying": { 153 | # Allow requests to other than "origin" server. 154 | self.servers["allowing"].url: ResourceOptions( 155 | allow_credentials=True, expose_headers="*", allow_headers="*" 156 | ) 157 | }, 158 | "free_for_all": { 159 | "*": ResourceOptions( 160 | allow_credentials=True, expose_headers="*", allow_headers="*" 161 | ) 162 | }, 163 | } 164 | 165 | # Configure CORS. 166 | for server_name, server_descr in self.servers.items(): 167 | default_config = cors_default_configs.get(server_name) 168 | if default_config is None: 169 | continue 170 | server_descr.cors = _setup(server_descr.app, defaults=default_config) 171 | 172 | # Add CORS routes. 173 | for server_name in cors_server_names: 174 | server_descr = self.servers[server_name] 175 | # TODO: Starting from aiohttp 0.21.0 name-based access returns 176 | # Resource, not Route. Manually get route while aiohttp_cors 177 | # doesn't support configuring for Resources. 178 | resource = server_descr.app.router["cors_resource"] 179 | route = next(iter(resource)) 180 | if self.use_resources: 181 | server_descr.cors.add(resource) 182 | server_descr.cors.add(route) 183 | 184 | elif self.use_webview: 185 | server_descr.cors.add(route) 186 | 187 | else: 188 | server_descr.cors.add(route) 189 | 190 | # Start servers. 191 | for server_name, server_descr in self.servers.items(): 192 | runner = web.AppRunner(server_descr.app) 193 | await runner.setup() 194 | site = web.SockSite(runner, server_sockets[server_name]) 195 | await site.start() 196 | server_descr.runner = runner 197 | 198 | self._logger.info( 199 | "Started server '%s' at '%s'", server_name, server_descr.url 200 | ) 201 | 202 | async def stop_servers(self): 203 | for server_descr in self.servers.values(): 204 | runner = server_descr.runner 205 | await runner.shutdown() 206 | await runner.cleanup() 207 | 208 | self.servers = {} 209 | 210 | 211 | def _get_chrome_driver(): 212 | driver_path_env = "WEBDRIVER_CHROMEDRIVER_PATH" 213 | 214 | if driver_path_env in os.environ: 215 | driver = webdriver.Chrome(executable_path=os.environ[driver_path_env]) 216 | else: 217 | driver = webdriver.Chrome() 218 | 219 | return driver 220 | 221 | 222 | @pytest.fixture(params=[(False, False), (True, False), (False, True)]) 223 | def server(request, loop): 224 | async def inner(): 225 | # to grab implicit loop 226 | return IntegrationServers(*request.param) 227 | 228 | return loop.run_until_complete(inner()) 229 | 230 | 231 | @pytest.fixture(params=[webdriver.Firefox, _get_chrome_driver]) 232 | def driver(request): 233 | try: 234 | driver = request.param() 235 | except selenium.common.exceptions.WebDriverException: 236 | pytest.skip("Driver is not supported") 237 | 238 | yield driver 239 | driver.close() 240 | 241 | 242 | async def test_in_webdriver(driver, server): 243 | loop = asyncio.get_event_loop() 244 | await server.start_servers() 245 | 246 | def selenium_thread(): 247 | driver.get(server.origin_server_url) 248 | assert "aiohttp_cors" in driver.title 249 | 250 | wait = WebDriverWait(driver, 10) 251 | 252 | run_button = wait.until(EC.element_to_be_clickable((By.ID, "runTestsButton"))) 253 | 254 | # Start tests. 255 | run_button.send_keys(Keys.RETURN) 256 | 257 | # Wait while test will finish (until clear button is not 258 | # activated). 259 | wait.until(EC.element_to_be_clickable((By.ID, "clearResultsButton"))) 260 | 261 | # Get results json 262 | results_area = driver.find_element(By.ID, "results") 263 | 264 | return json.loads(results_area.get_attribute("value")) 265 | 266 | try: 267 | results = await loop.run_in_executor(None, selenium_thread) 268 | 269 | assert results["status"] == "success" 270 | for test_name, test_data in results["data"].items(): 271 | assert test_data["status"] == "success" 272 | 273 | finally: 274 | await server.stop_servers() 275 | 276 | 277 | def _run_integration_server(): 278 | """Runs integration server for interactive debugging.""" 279 | 280 | logging.basicConfig(level=logging.INFO) 281 | 282 | logger = logging.getLogger("run_integration_server") 283 | 284 | loop = asyncio.get_event_loop() 285 | 286 | servers = IntegrationServers(False, True) 287 | logger.info("Starting integration servers...") 288 | loop.run_until_complete(servers.start_servers()) 289 | 290 | try: 291 | webbrowser.open(servers.origin_server_url) 292 | except webbrowser.Error: 293 | pass 294 | 295 | try: 296 | loop.run_forever() 297 | except KeyboardInterrupt: 298 | pass 299 | finally: 300 | logger.info("Stopping integration servers...") 301 | loop.run_until_complete(servers.stop_servers()) 302 | 303 | 304 | if __name__ == "__main__": 305 | # This module can be run in the following way: 306 | # $ python -m tests.integration.test_real_browser 307 | # from aiohttp_cors root directory. 308 | _run_integration_server() 309 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-cors/b800dee6906ef78c9ca8bc1edc2e3c917cff84a6/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test___about__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Vladimir Rutsky 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Test aiohttp_cors package metainformation.""" 16 | 17 | from packaging.version import parse 18 | 19 | import aiohttp_cors 20 | 21 | 22 | def test_version(): 23 | """Test package version string""" 24 | # not raised 25 | parse(aiohttp_cors.__version__) 26 | -------------------------------------------------------------------------------- /tests/unit/test_cors_config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Vladimir Rutsky 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """aiohttp_cors.cors_config unit tests.""" 16 | 17 | import pytest 18 | 19 | from aiohttp import web 20 | from aiohttp_cors import CorsConfig, CorsViewMixin, ResourceOptions 21 | 22 | 23 | async def _handler(request): 24 | return web.Response(text="Done") 25 | 26 | 27 | class _View(web.View, CorsViewMixin): 28 | 29 | async def get(self): 30 | return web.Response(text="Done") 31 | 32 | 33 | @pytest.fixture 34 | def app(): 35 | return web.Application() 36 | 37 | 38 | @pytest.fixture 39 | def cors(app): 40 | return CorsConfig(app, defaults={"*": ResourceOptions()}) 41 | 42 | 43 | @pytest.fixture 44 | def get_route(app): 45 | return app.router.add_route("GET", "/get_path", _handler) 46 | 47 | 48 | @pytest.fixture 49 | def options_route(app): 50 | return app.router.add_route("OPTIONS", "/options_path", _handler) 51 | 52 | 53 | def test_add_options_route(app, cors, options_route): 54 | """Test configuring OPTIONS route""" 55 | with pytest.raises(ValueError, match="already has OPTIONS handler"): 56 | cors.add(options_route.resource) 57 | 58 | 59 | def test_plain_named_route(app, cors): 60 | """Test adding plain named route.""" 61 | # Adding CORS routes should not introduce new named routes. 62 | assert len(app.router.keys()) == 0 63 | route = app.router.add_route("GET", "/{name}", _handler, name="dynamic_named_route") 64 | assert len(app.router.keys()) == 1 65 | cors.add(route) 66 | assert len(app.router.keys()) == 1 67 | 68 | 69 | def test_dynamic_named_route(app, cors): 70 | """Test adding dynamic named route.""" 71 | assert len(app.router.keys()) == 0 72 | route = app.router.add_route("GET", "/{name}", _handler, name="dynamic_named_route") 73 | assert len(app.router.keys()) == 1 74 | cors.add(route) 75 | assert len(app.router.keys()) == 1 76 | 77 | 78 | def test_static_named_route(app, cors): 79 | """Test adding dynamic named route.""" 80 | assert len(app.router.keys()) == 0 81 | route = app.router.add_static("/file", "/", name="dynamic_named_route") 82 | assert len(app.router.keys()) == 1 83 | cors.add(route) 84 | assert len(app.router.keys()) == 1 85 | 86 | 87 | def test_static_resource(app, cors): 88 | """Test adding static resource.""" 89 | assert len(app.router.keys()) == 0 90 | app.router.add_static("/file", "/", name="dynamic_named_route") 91 | assert len(app.router.keys()) == 1 92 | for resource in list(app.router.resources()): 93 | if isinstance(resource, web.StaticResource): 94 | cors.add(resource) 95 | assert len(app.router.keys()) == 1 96 | 97 | 98 | def test_web_view_resource(app, cors): 99 | """Test adding resource with web.View as handler""" 100 | assert len(app.router.keys()) == 0 101 | route = app.router.add_route("GET", "/{name}", _View, name="dynamic_named_route") 102 | assert len(app.router.keys()) == 1 103 | cors.add(route) 104 | assert len(app.router.keys()) == 1 105 | 106 | 107 | def test_web_view_warning(app, cors): 108 | """Test adding resource with web.View as handler""" 109 | route = app.router.add_route("*", "/", _View) 110 | with pytest.warns(DeprecationWarning): 111 | cors.add(route, webview=True) 112 | 113 | 114 | def test_disable_bare_view(app, cors): 115 | class View(web.View): 116 | pass 117 | 118 | route = app.router.add_route("*", "/", View) 119 | with pytest.raises(ValueError): 120 | cors.add(route) 121 | -------------------------------------------------------------------------------- /tests/unit/test_mixin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest import mock 3 | 4 | import pytest 5 | 6 | from aiohttp import web 7 | from aiohttp_cors import ( 8 | APP_CONFIG_KEY, 9 | CorsConfig, 10 | CorsViewMixin, 11 | ResourceOptions, 12 | custom_cors, 13 | ) 14 | 15 | DEFAULT_CONFIG = {"*": ResourceOptions()} 16 | 17 | CLASS_CONFIG = {"*": ResourceOptions()} 18 | 19 | CUSTOM_CONFIG = {"www.client1.com": ResourceOptions(allow_headers=["X-Host"])} 20 | 21 | 22 | class SimpleView(web.View, CorsViewMixin): 23 | async def get(self): 24 | return web.Response(text="Done") 25 | 26 | 27 | class SimpleViewWithConfig(web.View, CorsViewMixin): 28 | 29 | cors_config = CLASS_CONFIG 30 | 31 | async def get(self): 32 | return web.Response(text="Done") 33 | 34 | 35 | class CustomMethodView(web.View, CorsViewMixin): 36 | 37 | cors_config = CLASS_CONFIG 38 | 39 | async def get(self): 40 | return web.Response(text="Done") 41 | 42 | @custom_cors(CUSTOM_CONFIG) 43 | async def post(self): 44 | return web.Response(text="Done") 45 | 46 | 47 | @pytest.fixture 48 | def _app(): 49 | return web.Application() 50 | 51 | 52 | @pytest.fixture 53 | def cors(_app): 54 | ret = CorsConfig(_app, defaults=DEFAULT_CONFIG) 55 | _app[APP_CONFIG_KEY] = ret 56 | return ret 57 | 58 | 59 | @pytest.fixture 60 | def app(_app, cors): 61 | # a trick to install a cors into app 62 | return _app 63 | 64 | 65 | def test_raise_exception_when_cors_not_configure(): 66 | request = mock.Mock() 67 | request.app = {} 68 | view = CustomMethodView(request) 69 | 70 | with pytest.raises(ValueError): 71 | view.get_request_config(request, "post") 72 | 73 | 74 | async def test_raises_forbidden_when_config_not_found(app): 75 | app[APP_CONFIG_KEY].defaults = {} 76 | request = mock.Mock() 77 | request.app = app 78 | request.headers = {"Origin": "*", "Access-Control-Request-Method": "GET"} 79 | view = SimpleView(request) 80 | 81 | with pytest.raises(web.HTTPForbidden): 82 | await view.options() 83 | 84 | 85 | def test_method_with_custom_cors(app): 86 | """Test adding resource with web.View as handler""" 87 | request = mock.Mock() 88 | request.app = app 89 | view = CustomMethodView(request) 90 | 91 | assert hasattr(view.post, "post_cors_config") 92 | assert asyncio.iscoroutinefunction(view.post) 93 | config = view.get_request_config(request, "post") 94 | 95 | assert config.get("www.client1.com") == CUSTOM_CONFIG["www.client1.com"] 96 | 97 | 98 | def test_method_with_class_config(app): 99 | """Test adding resource with web.View as handler""" 100 | request = mock.Mock() 101 | request.app = app 102 | view = SimpleViewWithConfig(request) 103 | 104 | assert not hasattr(view.get, "get_cors_config") 105 | config = view.get_request_config(request, "get") 106 | 107 | assert config.get("*") == CLASS_CONFIG["*"] 108 | 109 | 110 | def test_method_with_default_config(app): 111 | """Test adding resource with web.View as handler""" 112 | request = mock.Mock() 113 | request.app = app 114 | view = SimpleView(request) 115 | 116 | assert not hasattr(view.get, "get_cors_config") 117 | config = view.get_request_config(request, "get") 118 | 119 | assert config.get("*") == DEFAULT_CONFIG["*"] 120 | -------------------------------------------------------------------------------- /tests/unit/test_preflight_handler.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from aiohttp_cors.preflight_handler import _PreflightHandler 6 | 7 | 8 | async def test_raises_when_handler_not_extend(): 9 | request = mock.Mock() 10 | handler = _PreflightHandler() 11 | with pytest.raises(NotImplementedError): 12 | await handler._get_config(request, "origin", "GET") 13 | -------------------------------------------------------------------------------- /tests/unit/test_resource_options.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Vladimir Rutsky 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """aiohttp_cors.resource_options unit tests.""" 16 | 17 | import pytest 18 | 19 | from aiohttp_cors.resource_options import ResourceOptions 20 | 21 | 22 | def test_init_no_args(): 23 | """Test construction without arguments""" 24 | opts = ResourceOptions() 25 | 26 | assert not opts.allow_credentials 27 | assert not opts.expose_headers 28 | assert not opts.allow_headers 29 | assert opts.max_age is None 30 | 31 | 32 | def test_comparison(): 33 | assert ResourceOptions() == ResourceOptions() 34 | assert not (ResourceOptions() != ResourceOptions()) 35 | assert not (ResourceOptions(allow_credentials=True) == ResourceOptions()) 36 | assert ResourceOptions(allow_credentials=True) != ResourceOptions() 37 | 38 | 39 | def test_allow_methods(): 40 | assert ResourceOptions().allow_methods is None 41 | assert ResourceOptions(allow_methods="*").allow_methods == "*" 42 | assert ResourceOptions(allow_methods=[]).allow_methods == frozenset() 43 | assert ResourceOptions(allow_methods=["get"]).allow_methods == frozenset(["GET"]) 44 | assert ResourceOptions(allow_methods=["get", "Post"]).allow_methods == { 45 | "GET", 46 | "POST", 47 | } 48 | with pytest.raises(ValueError): 49 | ResourceOptions(allow_methods="GET") 50 | 51 | 52 | # TODO: test arguments parsing 53 | -------------------------------------------------------------------------------- /tests/unit/test_urldispatcher_router_adapter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Vladimir Rutsky 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """aiohttp_cors.urldispatcher_router_adapter unit tests.""" 16 | 17 | from unittest import mock 18 | 19 | import pytest 20 | 21 | from aiohttp import web 22 | from aiohttp_cors import ResourceOptions 23 | from aiohttp_cors.urldispatcher_router_adapter import ( 24 | ResourcesUrlDispatcherRouterAdapter, 25 | ) 26 | 27 | 28 | async def _handler(request): 29 | return web.Response(text="Done") 30 | 31 | 32 | @pytest.fixture 33 | def app(): 34 | return web.Application() 35 | 36 | 37 | @pytest.fixture 38 | def adapter(app): 39 | return ResourcesUrlDispatcherRouterAdapter( 40 | app.router, defaults={"*": ResourceOptions()} 41 | ) 42 | 43 | 44 | @pytest.fixture 45 | def get_route(app): 46 | return app.router.add_route("GET", "/get_path", _handler) 47 | 48 | 49 | @pytest.fixture 50 | def options_route(app): 51 | return app.router.add_route("OPTIONS", "/options_path", _handler) 52 | 53 | 54 | def test_add_get_route(adapter, get_route): 55 | """Test configuring GET route""" 56 | result = adapter.add_preflight_handler(get_route.resource, _handler) 57 | assert result is None 58 | 59 | assert len(adapter._resource_config) == 0 60 | assert len(adapter._resources_with_preflight_handlers) == 1 61 | assert len(adapter._preflight_routes) == 1 62 | 63 | 64 | def test_add_options_route(adapter, options_route): 65 | """Test configuring OPTIONS route""" 66 | 67 | adapter.add_preflight_handler(options_route, _handler) 68 | 69 | assert not adapter._resources_with_preflight_handlers 70 | assert not adapter._preflight_routes 71 | 72 | 73 | def test_get_non_preflight_request_config(adapter, get_route): 74 | adapter.add_preflight_handler(get_route.resource, _handler) 75 | adapter.set_config_for_routing_entity( 76 | get_route.resource, 77 | { 78 | "http://example.org": ResourceOptions(), 79 | }, 80 | ) 81 | 82 | adapter.add_preflight_handler(get_route, _handler) 83 | adapter.set_config_for_routing_entity( 84 | get_route, 85 | { 86 | "http://test.example.org": ResourceOptions(), 87 | }, 88 | ) 89 | 90 | request = mock.Mock() 91 | 92 | with mock.patch( 93 | "aiohttp_cors.urldispatcher_router_adapter." 94 | "ResourcesUrlDispatcherRouterAdapter." 95 | "is_cors_enabled_on_request" 96 | ) as is_cors_enabled_on_request, mock.patch( 97 | "aiohttp_cors.urldispatcher_router_adapter." 98 | "ResourcesUrlDispatcherRouterAdapter." 99 | "_request_resource" 100 | ) as _request_resource: 101 | is_cors_enabled_on_request.return_value = True 102 | _request_resource.return_value = get_route.resource 103 | 104 | assert adapter.get_non_preflight_request_config(request) == { 105 | "*": ResourceOptions(), 106 | "http://example.org": ResourceOptions(), 107 | } 108 | 109 | request.method = "GET" 110 | 111 | assert adapter.get_non_preflight_request_config(request) == { 112 | "*": ResourceOptions(), 113 | "http://example.org": ResourceOptions(), 114 | "http://test.example.org": ResourceOptions(), 115 | } 116 | --------------------------------------------------------------------------------