././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5546324 CherryPy-18.9.0/0000755252176402575230000000000014536340235014235 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.appveyor.yml0000644252176402575230000000222314424762003016677 0ustar00jaracoprimarygroup# yamllint disable rule:line-length --- environment: matrix: - PYTHON: "C:\\Python37-x64" - PYTHON: "C:\\Python36-x64" init: - "chcp 65001" - ps: >- if($env:APPVEYOR_RDP_DEBUG -eq 'True') { iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) } install: # symlink python from a directory with a space - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" - "SET PYTHON=\"C:\\Program Files\\Python\"" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python -m pip install tox" - "python -m tox --notest" before_build: - "python -m pip install wheel" build_script: - python -m setup bdist_wheel test_script: - tox on_finish: - ps: >- if($env:APPVEYOR_RDP_DEBUG -eq 'True') { $blockRdp = $true iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) } - ps: | $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\.test-results\pytest\results.xml)) artifacts: - path: dist\* ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5139818 CherryPy-18.9.0/.circleci/0000755252176402575230000000000014536340235016070 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.circleci/config.yml0000644252176402575230000000222414424762003020055 0ustar00jaracoprimarygroup--- version: 2 jobs: macos-build: macos: xcode: "12.2.0" steps: - run: brew install pyenv readline xz - run: |- # https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables echo ' export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" ' >> $BASH_ENV - run: |- for py_ver in 3.7.0 3.6.4 do pyenv install "$py_ver" & done wait - run: pyenv global 3.7.0 3.6.4 - run: python3 -m pip install --upgrade pip wheel - run: python3 -m pip install tox tox-pyenv - checkout - run: tox -e py36,py37 -- -p no:sugar - store_test_results: path: .test-results - store_artifacts: path: .test-results linux-build: docker: - image: randomknowledge/docker-pyenv-tox steps: - checkout - run: pip install tox - run: tox -e py36,py37 - store_test_results: path: .test-results - store_artifacts: path: .test-results workflows: version: 2 test-linux-and-macos: jobs: - macos-build - linux-build ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.codecov.yml0000644252176402575230000000067214424762003016462 0ustar00jaracoprimarygroup--- codecov: bot: codecov notify: require_ci_to_pass: yes coverage: precision: 2 round: down range: "70...100" status: # Only consider coverage of the code snippet changed in PR project: no patch: yes changes: no parsers: gcov: branch_detection: conditional: yes loop: yes method: no macro: no comment: layout: "header, diff" behavior: default require_changes: no ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.flake80000644252176402575230000000027614307416152015413 0ustar00jaracoprimarygroup[flake8] ignore = # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 W504 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.gitattributes0000644252176402575230000000003114307416152017120 0ustar00jaracoprimarygroup/CHANGES.rst merge=union ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5157814 CherryPy-18.9.0/.github/0000755252176402575230000000000014536340235015575 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.github/CONTRIBUTING.rst0000644252176402575230000000230214307416152020231 0ustar00jaracoprimarygroupRead and contribute to CherryPy ------------------------------- Make sure you read the `README `_. Also ensure you set up `pre-commit utility `_ **correctly** and **TravisCI tests pass**:: pre-commit run --all-files # runs the same checks as in CI locally pre-commit install # sets up itself as a pre-commit hook of your local repo Submitting Pull Requests ------------------------ If you're changing the structure of the repository please create an issue first. Don't forget to write appropriate test cases, add them into CI process if applicable and make the TravisCI build pass. Sync (preferably rebase) your feature branch with upstream regularly to make us able to merge your PR seamlessly. Submitting bug reports ---------------------- Make sure you are on latest changes and that you re-ran this command `tox` after updating your local repository. If you can, please provide more information about your environment such as browser, operating system, python version, and any other related software versions. Also ---- See `Contributing `_ in the docs. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.github/FUNDING.yml0000644252176402575230000000044314424762003017410 0ustar00jaracoprimarygroup--- github: - jaraco - webknjaz # open_collective: # Replace with a single Open Collective username # ko_fi: # Replace with a single Ko-fi username tidelift: pypi/CherryPy # liberapay: # Replace with a single Liberapay username # custom: # Replace with up to 4 custom sponsorship URLs ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.github/ISSUE_TEMPLATE.md0000644252176402575230000000236114424762003020301 0ustar00jaracoprimarygroup **I'm submitting a ...** - [ ] bug report - [ ] feature request - [ ] question about the decisions made in the repository **Do you want to request a *feature* or report a *bug*?** **What is the current behavior?** **If the current behavior is a bug, please provide the steps to reproduce and if possible a screenshots and logs of the problem. If you can, show us your code.** **What is the expected behavior?** **What is the motivation / use case for changing the behavior?** **Please tell us about your environment:** - Cheroot version: X.X.X - CherryPy version: X.X.X - Python version: 3.6.X - OS: XXX - Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, e.g. stackoverflow, gitter, etc.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.github/PULL_REQUEST_TEMPLATE.md0000644252176402575230000000220214307416152021370 0ustar00jaracoprimarygroup**What kind of change does this PR introduce?** - [ ] bug fix - [ ] feature - [ ] docs update - [ ] tests/coverage improvement - [ ] refactoring - [ ] other **What is the related issue number (starting with `#`)** **What is the current behavior?** (You can also link to an open issue here) **What is the new behavior (if this is a feature change)?** **Other information**: **Checklist**: - [ ] I think the code is well written - [ ] I wrote [good commit messages][1] - [ ] I have [squashed related commits together][2] after the changes have been approved - [ ] Unit tests for the changes exist - [ ] Integration tests for the changes exist (if applicable) - [ ] I used the same coding conventions as the rest of the project - [ ] The new code doesn't generate linter offenses - [ ] Documentation reflects the changes - [ ] The PR relates to *only* one subject with a clear title and description in grammatically correct, complete sentences [1]: http://chris.beams.io/posts/git-commit/ [2]: https://github.com/todotxt/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.github/SUPPORT.rst0000644252176402575230000000345614307416152017511 0ustar00jaracoprimarygroupSupport ======= You've read the documentation and you've brushed up on the basics of Python and web development, but you still could use some help. Users have several options. I have a question ----------------- If you have a question and cannot find an answer for it in issues or the the `documentation `__, `please create an issue `__. Questions and their answers have great value for the community, and a tip is to really put the effort in and write a good explanation, you will get better and quicker answers. Examples are strongly encouraged. I have found a bug ------------------ If no one have already, `create an issue `__. Be sure to provide ample information, remember that any help won't be better than your explanation. Unless something is very obviously wrong, you are likely to be asked to provide a working example, displaying the erroneous behaviour. Note: While this might feel troublesome, a tip is to always make a separate example that have the same dependencies as your project. It is great for troubleshooting those annoying problems where you don't know if the problem is at your end or the components. Also, you can then easily fork and provide as an example. You will get answers and resolutions way quicker. Also, many other open source projects require it. I have a feature request ------------------------ `Good stuff! Please create an issue! `__\ Note: Features are more likely to be added the more users they seem to benefit. I want to converse ------------------ `The gitter page `__ is good for when you want to discuss in real time or get pointed in the right direction. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.github/config.yml0000644252176402575230000000004314424762003017557 0ustar00jaracoprimarygroup--- rtd: project: cherrypy ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.github/patchback.yml0000644252176402575230000000016314307416152020236 0ustar00jaracoprimarygroup--- backport_branch_prefix: patchback/backports/ backport_label_prefix: backport- target_branch_prefix: maint/ ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5159397 CherryPy-18.9.0/.github/workflows/0000755252176402575230000000000014536340235017632 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684327403.0 CherryPy-18.9.0/.github/workflows/ci-cd.yml0000644252176402575230000010563014431145753021343 0ustar00jaracoprimarygroup--- name: CI/CD on: # yamllint disable-line rule:truthy push: branches-ignore: - dependabot/** - maintenance/pip-tools-constraint-lockfiles - maintenance/pip-tools-constraint-lockfiles-** - patchback/backports/** - pre-commit-ci-update-config pull_request: workflow_dispatch: inputs: release-version: # github.event_name == 'workflow_dispatch' # && github.event.inputs.release-version description: >- Target PEP440-compliant version to release. Please, don't prepend `v`. required: true type: string release-committish: # github.event_name == 'workflow_dispatch' # && github.event.inputs.release-committish default: '' description: >- The commit to be released to PyPI and tagged in Git as `release-version`. Normally, you should keep this empty. type: string YOLO: default: false description: >- Flag whether test results should block the release. Only use this under extraordinary circumstances to ignore the test failures and cut the release regardless. type: boolean schedule: - cron: 1 0 * * * # Run daily at 0:01 UTC concurrency: group: >- ${{ github.workflow }}-${{ github.ref_type }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: dists-artifact-name: python-package-distributions dists-name: CherryPy FORCE_COLOR: 1 # Request colored output from CLI tools supporting it MYPY_FORCE_COLOR: 1 # MyPy's color enforcement PIP_DISABLE_PIP_VERSION_CHECK: 1 PIP_NO_PYTHON_VERSION_WARNING: 1 PIP_NO_WARN_SCRIPT_LOCATION: 1 PRE_COMMIT_COLOR: always PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest` PYTHONIOENCODING: utf-8 PYTHONLEGACYWINDOWSSTDIO: 1 # Python 3.6 hack PYTHONUTF8: 1 TOX_PARALLEL_NO_SPINNER: 1 TOX_TESTENV_PASSENV: >- # Make tox-wrapped tools see color requests FORCE_COLOR MYPY_FORCE_COLOR NO_COLOR PRE_COMMIT_COLOR PY_COLORS PYTEST_THEME PYTEST_THEME_MODE PYTHONIOENCODING PYTHONLEGACYWINDOWSSTDIO PYTHONUTF8 TOX_VERSION: tox < 4 run-name: >- ${{ github.event_name == 'workflow_dispatch' && format('📦 Releasing v{0}...', github.event.inputs.release-version) || '' }} ${{ github.event.pull_request.number && 'PR' || '' }}${{ !github.event.pull_request.number && 'Commit' || '' }} ${{ github.event.pull_request.number || github.sha }} triggered by: ${{ github.event_name }} of ${{ github.ref }} ${{ github.ref_type }} (workflow run ID: ${{ github.run_id }}; number: ${{ github.run_number }}; attempt: ${{ github.run_attempt }}) jobs: pre-setup: name: ⚙️ Mode runs-on: ubuntu-latest defaults: run: shell: python outputs: dist-name: ${{ steps.dist.outputs.name }} dist-version: >- ${{ steps.request-check.outputs.release-requested == 'true' && github.event.inputs.release-version || steps.scm-version.outputs.dist-version }} is-untagged-devel: >- ${{ steps.untagged-check.outputs.is-untagged-devel || false }} release-requested: >- ${{ steps.request-check.outputs.release-requested || false }} is-yolo-mode: >- ${{ ( steps.request-check.outputs.release-requested == 'true' && github.event.inputs.YOLO ) && true || false }} cache-key-files: >- ${{ steps.calc-cache-key-files.outputs.files-hash-key }} git-tag: ${{ steps.git-tag.outputs.tag }} sdist-artifact-name: ${{ steps.artifact-name.outputs.sdist }} wheel-artifact-name: ${{ steps.artifact-name.outputs.wheel }} steps: - name: Switch to using Python 3.11 by default uses: actions/setup-python@v4 with: python-version: 3.11 - name: >- Mark the build as untagged '${{ github.event.repository.default_branch }}' branch build id: untagged-check if: >- github.event_name == 'push' && github.ref == format( 'refs/heads/{0}', github.event.repository.default_branch ) run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print('is-untagged-devel=true', file=outputs_file) - name: Mark the build as "release request" id: request-check if: github.event_name == 'workflow_dispatch' run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print('release-requested=true', file=outputs_file) - name: Check out src from Git if: >- steps.request-check.outputs.release-requested != 'true' uses: actions/checkout@v3 with: fetch-depth: >- ${{ steps.request-check.outputs.release-requested == 'true' && 1 || 0 }} ref: ${{ github.event.inputs.release-committish }} - name: >- Calculate Python interpreter version hash value for use in the cache key if: >- steps.request-check.outputs.release-requested != 'true' id: calc-cache-key-py run: | from hashlib import sha512 from os import environ from pathlib import Path from sys import version FILE_APPEND_MODE = 'a' hash = sha512(version.encode()).hexdigest() with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print(f'py-hash-key={hash}', file=outputs_file) - name: >- Calculate dependency files' combined hash value for use in the cache key if: >- steps.request-check.outputs.release-requested != 'true' id: calc-cache-key-files run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print( "files-hash-key=${{ hashFiles( 'setup.cfg', 'setup.py', 'tox.ini', 'pyproject.toml', '.pre-commit-config.yaml', 'pytest.ini' ) }}", file=outputs_file, ) - name: Get pip cache dir id: pip-cache-dir if: >- steps.request-check.outputs.release-requested != 'true' run: >- echo "dir=$(python -m pip cache dir)" >> "${GITHUB_OUTPUT}" shell: bash - name: Set up pip cache if: >- steps.request-check.outputs.release-requested != 'true' uses: actions/cache@v3 with: path: ${{ steps.pip-cache-dir.outputs.dir }} key: >- ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}-${{ steps.calc-cache-key-files.outputs.files-hash-key }} restore-keys: | ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}- ${{ runner.os }}-pip- ${{ runner.os }}- - name: Drop Git tags from HEAD for non-release requests if: >- steps.request-check.outputs.release-requested != 'true' run: >- git tag --points-at HEAD | xargs git tag --delete shell: bash - name: Set up versioning prerequisites if: >- steps.request-check.outputs.release-requested != 'true' run: >- python -m pip install --user setuptools-scm shell: bash - name: Set the current dist version from Git if: steps.request-check.outputs.release-requested != 'true' id: scm-version run: | from os import environ from pathlib import Path import setuptools_scm FILE_APPEND_MODE = 'a' ver = setuptools_scm.get_version( ${{ steps.untagged-check.outputs.is-untagged-devel == 'true' && 'local_scheme="no-local-version"' || '' }} ) with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print(f'dist-version={ver}', file=outputs_file) - name: Set the target Git tag id: git-tag run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print( "tag=v${{ steps.request-check.outputs.release-requested == 'true' && github.event.inputs.release-version || steps.scm-version.outputs.dist-version }}", file=outputs_file, ) - name: Set the expected dist name id: dist run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print("name=${{ env.dists-name }}", file=outputs_file) - name: Set the expected dist artifact names id: artifact-name run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print( "sdist=${{ steps.dist.outputs.name }}-${{ steps.request-check.outputs.release-requested == 'true' && github.event.inputs.release-version || steps.scm-version.outputs.dist-version }}.tar.gz", file=outputs_file, ) print( "wheel=${{ steps.dist.outputs.name }}-${{ steps.request-check.outputs.release-requested == 'true' && github.event.inputs.release-version || steps.scm-version.outputs.dist-version }}-py3-none-any.whl", file=outputs_file, ) build: name: >- 👷 dists ${{ needs.pre-setup.outputs.git-tag }} [mode: ${{ fromJSON(needs.pre-setup.outputs.is-untagged-devel) && 'nightly' || '' }}${{ fromJSON(needs.pre-setup.outputs.release-requested) && 'release' || '' }}${{ ( !fromJSON(needs.pre-setup.outputs.is-untagged-devel) && !fromJSON(needs.pre-setup.outputs.release-requested) ) && 'test' || '' }}] needs: - pre-setup # transitive, for accessing settings runs-on: ubuntu-latest env: TOXENV: cleanup-dists,build-dists steps: - name: Switch to using Python 3.11 uses: actions/setup-python@v4 with: python-version: 3.11 - name: Grab the source from Git uses: actions/checkout@v3 with: fetch-depth: >- ${{ fromJSON(needs.pre-setup.outputs.release-requested) && 1 || 0 }} ref: ${{ github.event.inputs.release-committish }} - name: >- Calculate Python interpreter version hash value for use in the cache key id: calc-cache-key-py run: | from hashlib import sha512 from os import environ from pathlib import Path from sys import version FILE_APPEND_MODE = 'a' hash = sha512(version.encode()).hexdigest() with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print(f'py-hash-key={hash}', file=outputs_file) shell: python - name: Get pip cache dir id: pip-cache-dir run: >- echo "dir=$(python -m pip cache dir)" >> "${GITHUB_OUTPUT}" - name: Set up pip cache uses: actions/cache@v3 with: path: ${{ steps.pip-cache-dir.outputs.dir }} key: >- ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}-${{ needs.pre-setup.outputs.cache-key-files }} restore-keys: | ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}- ${{ runner.os }}-pip- - name: Install tox run: >- python -m pip install --user '${{ env.TOX_VERSION }}' - name: Pre-populate the tox env run: >- python -m tox --parallel auto --parallel-live --skip-missing-interpreters false --notest - name: Drop Git tags from HEAD for non-tag-create events if: >- !fromJSON(needs.pre-setup.outputs.release-requested) run: >- git tag --points-at HEAD | xargs git tag --delete shell: bash - name: Setup git user as [bot] if: >- fromJSON(needs.pre-setup.outputs.release-requested) || fromJSON(needs.pre-setup.outputs.is-untagged-devel) uses: fregante/setup-git-user@v1.1.0 - name: >- Tag the release in the local Git repo as ${{ needs.pre-setup.outputs.git-tag }} for setuptools-scm to set the desired version if: >- fromJSON(needs.pre-setup.outputs.release-requested) run: >- git tag -m '${{ needs.pre-setup.outputs.git-tag }}' '${{ needs.pre-setup.outputs.git-tag }}' -- ${{ fromJSON(needs.pre-setup.outputs.release-requested) && github.event.inputs.release-committish || '' }} - name: Install tomlkit Python distribution package if: >- fromJSON(needs.pre-setup.outputs.is-untagged-devel) run: >- python -m pip install --user tomlkit - name: Instruct setuptools-scm not to add a local version part if: >- fromJSON(needs.pre-setup.outputs.is-untagged-devel) run: | from pathlib import Path import tomlkit pyproject_toml_path = Path.cwd() / 'pyproject.toml' pyproject_toml_txt = pyproject_toml_path.read_text() pyproject_toml = tomlkit.loads(pyproject_toml_txt) setuptools_scm_section = pyproject_toml['tool']['setuptools_scm'] setuptools_scm_section['local_scheme'] = 'no-local-version' patched_pyproject_toml_txt = tomlkit.dumps(pyproject_toml) pyproject_toml_path.write_text(patched_pyproject_toml_txt) shell: python - name: Pretend that pyproject.toml is unchanged if: >- fromJSON(needs.pre-setup.outputs.is-untagged-devel) run: | git diff --color=always git update-index --assume-unchanged pyproject.toml - name: Build dists run: >- python -m tox --parallel auto --parallel-live --skip-missing-interpreters false --skip-pkg-install - name: Verify that the artifacts with expected names got created run: >- ls -1 'dist/${{ needs.pre-setup.outputs.sdist-artifact-name }}' 'dist/${{ needs.pre-setup.outputs.wheel-artifact-name }}' - name: Store the distribution packages uses: actions/upload-artifact@v3 with: name: ${{ env.dists-artifact-name }} # NOTE: Exact expected file names are specified here # NOTE: as a safety measure — if anything weird ends # NOTE: up being in this dir or not all dists will be # NOTE: produced, this will fail the workflow. path: | dist/${{ needs.pre-setup.outputs.sdist-artifact-name }} dist/${{ needs.pre-setup.outputs.wheel-artifact-name }} retention-days: >- ${{ ( fromJSON(needs.pre-setup.outputs.release-requested) ) && 90 || 30 }} lint: name: 🧹 ${{ matrix.toxenv }} needs: - build - pre-setup # transitive, for accessing settings runs-on: ${{ matrix.os }}-latest strategy: matrix: os: - ubuntu python-version: - 3.11 toxenv: - build-docs - dist-check # - doctest-docs # - linkcheck-docs # - metadata-validation - pre-commit - pre-commit-pep257 - setup-check # - spellcheck-docs fail-fast: false env: TOXENV: ${{ matrix.toxenv }} steps: - name: >- Switch to using Python v${{ matrix.python-version }} by default uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # NOTE: `pre-commit --show-diff-on-failure` and `sphinxcontrib-spellcheck` # NOTE: with Git authors allowlist enabled both depend on the presence of a # NOTE: Git repository. - name: Grab the source from Git if: >- contains( fromJSON('["pre-commit", "pre-commit-pep257", "spellcheck-docs"]'), matrix.toxenv ) uses: actions/checkout@v3 with: ref: ${{ github.event.inputs.release-committish }} - name: Retrieve the project source from an sdist inside the GHA artifact if: >- !contains( fromJSON('["pre-commit", "pre-commit-pep257", "spellcheck-docs"]'), matrix.toxenv ) uses: re-actors/checkout-python-sdist@release/v1 with: source-tarball-name: ${{ needs.pre-setup.outputs.sdist-artifact-name }} workflow-artifact-name: ${{ env.dists-artifact-name }} - name: >- Calculate Python interpreter version hash value for use in the cache key id: calc-cache-key-py run: | from hashlib import sha512 from os import environ from pathlib import Path from sys import version FILE_APPEND_MODE = 'a' hash = sha512(version.encode()).hexdigest() with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, ) as outputs_file: print(f'py-hash-key={hash}', file=outputs_file) shell: python - name: Get pip cache dir id: pip-cache run: >- echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" - name: Set up pip cache uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: >- ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}-${{ needs.pre-setup.outputs.cache-key-files }} restore-keys: | ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}- ${{ runner.os }}-pip- - name: Install tox run: >- python -m pip install --user '${{ env.TOX_VERSION }}' - name: Make the env clean of non-test files if: matrix.toxenv == 'metadata-validation' run: | shopt -s extglob rm -rf !tox.ini shell: bash - name: Download all the dists if: matrix.toxenv == 'metadata-validation' uses: actions/download-artifact@v3 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: >- Pre-populate tox envs: `${{ env.TOXENV }}` run: >- python -m tox --parallel auto --parallel-live --skip-missing-interpreters false --notest - name: Initialize pre-commit envs if needed run: >- test -d .tox/pre-commit && .tox/pre-commit/bin/python -m pre_commit install-hooks || : - name: >- Run tox envs: `${{ env.TOXENV }}` run: >- python -m tox --parallel auto --parallel-live --skip-missing-interpreters false --skip-pkg-install tests: name: >- 🧪 🐍${{ matrix.python-version }} @ ${{ matrix.os }} needs: - build - pre-setup # transitive, for accessing settings runs-on: ${{ matrix.os }} strategy: matrix: python-version: # NOTE: The latest and the lowest supported Pythons are prioritized # NOTE: to improve the responsiveness. It's nice to see the most # NOTE: important results first. - 3.11 - pypy-3.9 - >- 3.10 - 3.9 - 3.8 - 3.7 - 3.6 - pypy-3.6 - ~3.12.0-0 os: - ubuntu-22.04 - macos-12 - windows-2022 - ubuntu-20.04 - macos-11 - windows-2019 exclude: # NOTE: Windows PyPy jobs are excluded to address the tox bug # NOTE: https://github.com/tox-dev/tox/issues/1704. # NOTE: They should be re-added once it's fixed. - os: windows-2022 python-version: pypy-3.6 - os: windows-2019 python-version: pypy-3.6 # NOTE: Windows PyPy 3.7 jobs are excluded because of the lack # NOTE: of the build deps to compile cryptography. # NOTE: They should be re-added once it's fixed. - os: windows-2022 python-version: pypy-3.9 - os: windows-2019 python-version: pypy-3.9 # NOTE: macOS PyPy jobs are excluded because installing cryptography # NOTE: needs openssl headers that aren't present at the moment. # TODO: Remove the exclusions once this is addressed. - os: macos-11 python-version: pypy-3.6 - os: macos-latest python-version: pypy-3.6 - os: macos-11 python-version: pypy-3.9 - os: macos-12 python-version: pypy-3.9 - os: ubuntu-22.04 python-version: 3.6 # EOL, only provided for older OSs include: [] # TODO: include TOXENV=cheroot-master continue-on-error: >- ${{ ( fromJSON(needs.pre-setup.outputs.is-yolo-mode) || ( startsWith(matrix.python-version, '~') ) || contains(matrix.python-version, 'alpha') ) && true || false }} env: TOXENV: python steps: - name: Set up Python ${{ matrix.python-version }} id: python-install uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Retrieve the project source from an sdist inside the GHA artifact uses: re-actors/checkout-python-sdist@release/v1 with: source-tarball-name: ${{ needs.pre-setup.outputs.sdist-artifact-name }} workflow-artifact-name: ${{ env.dists-artifact-name }} - name: Figure out if the interpreter ABI is stable id: py-abi run: | from os import environ from sys import version_info FILE_APPEND_MODE = 'a' is_stable_abi = version_info.releaselevel == 'final' with open( environ['GITHUB_OUTPUT'], mode=FILE_APPEND_MODE, ) as outputs_file: print( 'is-stable-abi={is_stable_abi}'. format(is_stable_abi=str(is_stable_abi).lower()), file=outputs_file, ) shell: python - name: >- Calculate Python interpreter version hash value for use in the cache key if: fromJSON(steps.py-abi.outputs.is-stable-abi) id: calc-cache-key-py run: | from hashlib import sha512 from os import environ from sys import version FILE_APPEND_MODE = 'a' hash = sha512(version.encode()).hexdigest() with open( environ['GITHUB_OUTPUT'], mode=FILE_APPEND_MODE, ) as outputs_file: print(f'py-hash-key={hash}', file=outputs_file) shell: python - name: Get pip cache dir if: fromJSON(steps.py-abi.outputs.is-stable-abi) id: pip-cache-dir run: >- echo "dir=$(python -m pip cache dir)" >> "${GITHUB_OUTPUT}" shell: bash - name: Set up pip cache if: fromJSON(steps.py-abi.outputs.is-stable-abi) uses: actions/cache@v3 with: path: ${{ steps.pip-cache-dir.outputs.dir }} key: >- ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}-${{ needs.pre-setup.outputs.cache-key-files }} restore-keys: | ${{ runner.os }}-pip-${{ steps.calc-cache-key-py.outputs.py-hash-key }}- ${{ runner.os }}-pip- - name: Upgrade pip with `requires_python` run: >- python -m pip install --user --upgrade --force-reinstall pip-with-requires-python - name: Install tox run: >- python -m pip install --user '${{ env.TOX_VERSION }}' - name: Patch tox.ini for Python 3.6 under Windows if: >- runner.os == 'Windows' && matrix.python-version == '3.6' run: >- sed -i 's/^package_env\(\s\)\?=.*/package_env = py36-win-dummy/g' tox.ini shell: bash - name: Download all the dists uses: actions/download-artifact@v3 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: >- Pre-populate tox env: ${{ env.TOXENV }} run: >- python -m tox --parallel auto --parallel-live --skip-missing-interpreters false --installpkg 'dist/${{ needs.pre-setup.outputs.wheel-artifact-name }}' --notest - name: Windows system info run: systeminfo if: >- startsWith(matrix.os, 'windows-') - name: >- Log platform.platform() run: >- python -m platform - name: >- Log platform.version() run: >- python -c "import platform; print(platform.version())" - name: >- Log platform.uname() run: >- python -c "import platform; print(platform.uname())" - name: >- Log platform.release() run: >- python -c "import platform; print(platform.release())" - name: Log stdlib OpenSSL version run: >- python -c "import ssl; print('\nOPENSSL_VERSION: ' + ssl.OPENSSL_VERSION + '\nOPENSSL_VERSION_INFO: ' + repr(ssl.OPENSSL_VERSION_INFO) + '\nOPENSSL_VERSION_NUMBER: ' + repr(ssl.OPENSSL_VERSION_NUMBER))" - name: Run the testing run: >- python -m tox --parallel auto --parallel-live --skip-missing-interpreters false --installpkg 'dist/${{ needs.pre-setup.outputs.wheel-artifact-name }}' - name: Produce markdown test summary from JUnit if: >- !cancelled() uses: test-summary/action@v2.0 with: paths: .test-results/pytest/results.xml - name: Check if Cobertura XML coverage files exist if: >- !cancelled() && runner.os == 'Linux' id: coverage-files run: >- compgen -G '.test-results/pytest/cov.xml' && ( echo 'present=true' >> ${GITHUB_OUTPUT} ) ; exit 0 shell: bash - name: Produce markdown test summary from Cobertura XML if: steps.coverage-files.outputs.present == 'true' uses: irongut/CodeCoverageSummary@v1.3.0 with: badge: true filename: .test-results/pytest/cov.xml format: markdown output: both # Ref: https://github.com/irongut/CodeCoverageSummary/issues/66 - name: Append coverage results to Job Summary if: steps.coverage-files.outputs.present == 'true' run: >- cat code-coverage-results.md >> "${GITHUB_STEP_SUMMARY}" - name: Re-run the failing tests with maximum verbosity if: >- !cancelled() && failure() run: >- # `exit 1` makes sure that the job remains red with flaky runs python -m tox --parallel auto --parallel-live --skip-missing-interpreters false -vvvvv --installpkg 'dist/${{ needs.pre-setup.outputs.wheel-artifact-name }}' -- --no-cov -vvvvv --lf && exit 1 shell: bash - name: Send coverage data to Codecov if: >- !cancelled() uses: codecov/codecov-action@v3 with: files: .test-results/pytest/cov.xml flags: >- CI-GHA, OS-${{ runner.os }}, VM-${{ matrix.os }}, Py-${{ steps.python-install.outputs.python-version }}, ${{ needs.pre-setup.outputs.wheel-artifact-name }} check: # This job does nothing and is only used for the branch protection if: always() needs: - lint - pre-setup # transitive, for accessing settings - tests runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: allowed-failures: >- ${{ fromJSON(needs.pre-setup.outputs.is-yolo-mode) && 'lint, tests' || '' }} jobs: ${{ toJSON(needs) }} publish-pypi: name: Publish 🐍📦 ${{ needs.pre-setup.outputs.git-tag }} to PyPI needs: - check - pre-setup # transitive, for accessing settings if: >- fromJSON(needs.pre-setup.outputs.release-requested) runs-on: ubuntu-latest environment: name: pypi url: >- https://pypi.org/project/${{ needs.pre-setup.outputs.dist-name }}/${{ needs.pre-setup.outputs.dist-version }} steps: - name: Download all the dists uses: actions/download-artifact@v3 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: >- Publish 🐍📦 ${{ needs.pre-setup.outputs.git-tag }} to PyPI uses: pypa/gh-action-pypi-publish@release/v1 publish-testpypi: name: Publish 🐍📦 ${{ needs.pre-setup.outputs.git-tag }} to TestPyPI needs: - check - pre-setup # transitive, for accessing settings if: >- fromJSON(needs.pre-setup.outputs.is-untagged-devel) || fromJSON(needs.pre-setup.outputs.release-requested) runs-on: ubuntu-latest environment: name: testpypi url: >- https://pypi.org/project/${{ needs.pre-setup.outputs.dist-name }}/${{ needs.pre-setup.outputs.dist-version }} steps: - name: Download all the dists uses: actions/download-artifact@v3 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: >- Publish 🐍📦 ${{ needs.pre-setup.outputs.git-tag }} to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository_url: https://test.pypi.org/legacy/ post-release-repo-update: name: >- Publish post-release Git tag for ${{ needs.pre-setup.outputs.git-tag }} needs: - publish-pypi - pre-setup # transitive, for accessing settings runs-on: ubuntu-latest steps: - name: >- Check if the requested tag ${{ needs.pre-setup.outputs.git-tag }} is present and is pointing at the required commit ${{ github.event.inputs.release-committish }} id: existing-remote-tag-check run: | REMOTE_TAGGED_COMMIT_SHA="$( git ls-remote --tags --refs $(git remote get-url origin) '${{ needs.pre-setup.outputs.git-tag }}' | awk '{print $1}' )" if [[ "${REMOTE_TAGGED_COMMIT_SHA}" == '${{ github.event.inputs.release-committish }}' ]] then echo "already-exists=true" >> "${GITHUB_OUTPUT}" fi - name: Fetch the src snapshot if: steps.existing-remote-tag-check.outputs.already-exists == 'true' uses: actions/checkout@v3 with: fetch-depth: 1 ref: ${{ github.event.inputs.release-committish }} - name: Setup git user as [bot] if: steps.existing-remote-tag-check.outputs.already-exists == 'true' uses: fregante/setup-git-user@v1.1.0 - name: >- Tag the release in the local Git repo as ${{ needs.pre-setup.outputs.git-tag }} if: steps.existing-remote-tag-check.outputs.already-exists == 'true' run: >- git tag -m '${{ needs.pre-setup.outputs.git-tag }}' -m 'Published at https://pypi.org/project/${{ needs.pre-setup.outputs.dist-name }}/${{ needs.pre-setup.outputs.dist-version }}' -m 'This release has been produced by the following workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' '${{ needs.pre-setup.outputs.git-tag }}' -- ${{ github.event.inputs.release-committish }} - name: >- Push ${{ needs.pre-setup.outputs.git-tag }} tag corresponding to the just published release back to GitHub if: steps.existing-remote-tag-check.outputs.already-exists == 'true' run: >- git push --atomic origin '${{ needs.pre-setup.outputs.git-tag }}' publish-github-release: name: >- Publish a GitHub Release for ${{ needs.pre-setup.outputs.git-tag }} needs: - post-release-repo-update - pre-setup # transitive, for accessing settings runs-on: ubuntu-latest permissions: contents: write discussions: write steps: - name: Download all the dists uses: actions/download-artifact@v3 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: >- Publish a GitHub Release for ${{ needs.pre-setup.outputs.git-tag }} uses: ncipollo/release-action@v1.11.1 with: allowUpdates: false artifactErrorsFailBuild: false artifacts: | dist/${{ needs.pre-setup.outputs.sdist-artifact-name }} dist/${{ needs.pre-setup.outputs.wheel-artifact-name }} artifactContentType: raw # Because whl and tgz are of different types body: > # Release ${{ needs.pre-setup.outputs.git-tag }} This release is published to https://pypi.org/project/${{ needs.pre-setup.outputs.dist-name }}/${{ needs.pre-setup.outputs.dist-version }}. This release has been produced by the following workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}. # bodyFile: # FIXME: Use once Towncrier is integrated. commit: ${{ github.event.inputs.release-committish }} discussionCategory: Announcements draft: false name: ${{ needs.pre-setup.outputs.git-tag }} # omitBody: false omitBodyDuringUpdate: true omitName: false omitNameDuringUpdate: true omitPrereleaseDuringUpdate: true prerelease: false removeArtifacts: false replacesArtifacts: false tag: ${{ needs.pre-setup.outputs.git-tag }} token: ${{ secrets.GITHUB_TOKEN }} ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.gitignore0000644252176402575230000000144414307416152016226 0ustar00jaracoprimarygroup*.egg-info *.pyc *.orig build/* dist/* *.swp *.log lib share bin *.kdev4 include sphinx/source/_build .tox /.eggs/ /.ropeproject/ /cherrypy/test/static/has space.html /cherrypy/test/test.conf # pyenv local config: .python-version # Ignore useless OS X's dirs: .DS_Store # Py.test /.cache/ /.pytest_cache/ # Eclipse, PyDev .project .pydevproject # test results in junit format for Appveyor /.test-results/ # coverage results /.coverage /coverage.xml # test artifacts /graph_AppResponse_*.png /graph_Response_*.png /graph_Request_*.png # Covers JetBrains IDEs: IntelliJ, PyCharm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/ # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # Editor-based Rest Client .idea/httpRequests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.pre-commit-config-pep257.yaml0000644252176402575230000000015214424762003021531 0ustar00jaracoprimarygroup--- repos: - repo: https://github.com/PyCQA/pydocstyle.git rev: 6.2.2 hooks: - id: pydocstyle ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.pre-commit-config.yaml0000644252176402575230000000330514424762003020514 0ustar00jaracoprimarygroup--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks.git rev: v1.1.1 hooks: - id: trailing-whitespace exclude: cherrypy/test/static/index.html - id: flake8 - id: check-merge-conflict - id: double-quote-string-fixer - id: end-of-file-fixer - id: name-tests-test include: cherrypy/test/ args: - --django exclude: tests/dist-check.py - id: debug-statements - id: check-added-large-files - id: check-ast - id: check-byte-order-marker - id: check-case-conflict - id: check-json include: .mention-bot - id: check-symlinks - id: check-yaml - id: detect-private-key exclude: cherrypy/test/test.pem - id: requirements-txt-fixer - repo: https://github.com/PyCQA/pydocstyle.git rev: 6.2.2 hooks: - id: pydocstyle exclude: | (?x) tests/dist| cherrypy/( _cp( compat|modpy|logging|error|wsgi|dispatch|tools|reqbody|request )| ( lib/( auth|auth_basic|auth_digest|caching|covercp| cpstats|cptools|encoding|gctools|httpauth|httputil| jsontools|lockfile|locking|profiler|reprconf|sessions )| process/(plugins|servers|win32) ).py| test|tutorial ) - repo: https://github.com/Lucas-C/pre-commit-hooks.git rev: v1.1.12 hooks: - id: remove-tabs - repo: https://github.com/Lucas-C/pre-commit-hooks-lxml.git rev: v1.1.0 hooks: - id: forbid-html-img-without-alt-text - repo: https://github.com/adrienverge/yamllint.git rev: v1.27.1 hooks: - id: yamllint files: \.(yaml|yml)$ types: [file, yaml] args: - --strict ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.readthedocs.yml0000644252176402575230000000016714424762003017324 0ustar00jaracoprimarygroup--- build: image: latest python: version: 3.6 extra_requirements: - docs - testing pip_install: true ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5084665 CherryPy-18.9.0/.test-results/0000755252176402575230000000000014536340235016771 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5164635 CherryPy-18.9.0/.test-results/pytest/0000755252176402575230000000000014536340235020321 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.test-results/pytest/.gitignore0000644252176402575230000000000214307416152022277 0ustar00jaracoprimarygroup* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.travis.yml0000644252176402575230000001164314424762003016350 0ustar00jaracoprimarygroup# yamllint disable rule:line-length --- conditions: v1 language: python os: linux dist: focal services: - memcached _base_envs: - &stage_lint stage: &stage_lint_name lint - &stage_test stage: &stage_test_name test - &stage_test_priority stage: &stage_test_priority_name test against latest Python versions first (under GNU/Linux) - &stage_test_osx stage: &stage_test_osx_name test under OS X (last chance to fail before deploy available) - &stage_deploy stage: &stage_deploy_name upload new version of python package to PYPI (only for tagged commits) - _conditions: - &condition_api_or_cron if: type IN (api, cron) - &no_memcached services: [] - &pyenv_base <<: *stage_test language: generic env: - &env_pyenv PYENV_ROOT="$HOME/.pyenv" - &env_path PATH="$PYENV_ROOT/bin:$PATH" before_install: - &ensure_pyenv_installed | if [ ! -f "$PYENV_ROOT/bin/pyenv" ] then rm -rf "$PYENV_ROOT" curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash fi eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pyenv update - &install_python pyenv install --skip-existing --keep --verbose "$PYTHON_VERSION" - &switch_python pyenv shell "$PYTHON_VERSION" - &python_version python --version - &osx_python_base <<: *pyenv_base <<: *stage_test_osx os: osx language: generic before_install: - brew update - brew install pyenv || brew upgrade pyenv - &ensure_pyenv_preloaded | eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" - *install_python - *switch_python - *python_version before_cache: - brew --cache script: - travis_retry python -m tox - &python_3_9_mixture python: &mainstream_python 3.9 group: - &pure_python_base <<: *stage_test <<: *python_3_9_mixture - &pure_python_base_priority <<: *pure_python_base <<: *stage_test_priority - &lint_python_base <<: *stage_lint <<: *no_memcached python: 3.9 after_failure: skip python: - 3.7-dev jobs: fast_finish: true include: - <<: *lint_python_base env: TOXENV=pre-commit - <<: *lint_python_base env: TOXENV=pre-commit-pep257 - <<: *lint_python_base env: TOXENV=dist-check - <<: *lint_python_base env: TOXENV=setup-check - <<: *lint_python_base name: >- Ensure that docs get built (non-strict until \#1797 get fixed) env: TOXENV=build-docs - <<: *pure_python_base_priority # mainstream Python here - <<: *pure_python_base_priority # mainstream Python here # run tests against the bleeding-edge cheroot env: TOXENV=cheroot-master - <<: *pure_python_base_priority python: nightly - <<: *osx_python_base python: *mainstream_python env: - PYTHON_VERSION=3.6.5 - *env_pyenv - *env_path - <<: *osx_python_base python: 3.7-dev env: - PYTHON_VERSION=3.7-dev - *env_pyenv - *env_path - <<: *osx_python_base # mainstream Python here python: *mainstream_python env: - PYTHON_VERSION=3.7.0 # run tests against the bleeding-edge cheroot - TOXENV=cheroot-master - *env_pyenv - *env_path - <<: *stage_deploy <<: *python_3_9_mixture <<: *no_memcached install: skip script: skip deploy: provider: pypi skip_cleanup: true on: tags: true all_branches: true python: *mainstream_python user: __token__ distributions: dists password: secure: r9jZVhWnwBpbQwkoAQnhcQajV6Hk8WKs53+P20YrNfLSrSfODmJFyljCLsUJH7TnmAdrnQfV19PXPfVXPucK2ZEg2E91/5z6pgADi01NX3QMr7vEpffk6ix0uHBSa3VMBF+VlmhCzAFnNIN0E7M4kjoc5Cr7qBWPwZpqd715axYxBKSIH4Cmt2cyW3ozMftNtbI+ujs+kJTX6m/2UAL8yngau0TWR5bUBaywTZdkfPIKxt2XDfTW5PuOTRgS6QSU+/Va+M1IJhFPthjmTO+t8U/qonSLA34nLkT7vJmME0lyQF0lr+IV41IKxEFz29hmzLY1dyZI5+bs3vEhxU1xGqwr1Hnif6f14TzeiubQrCxt1UP9D3HXguCNI4gGeny1OPJNNt5ezJDNha2HlIy2quLKgtW38TS0PPm7PDqgYhjidZyRXZ8G6A/DAwh00amCNkSN6lG7Lryd1QB44mYHCKm8XdLfBT94EqzSdgQyyoUAA87A8zB5zpHiRD2DGwrTxHkGQo7TTSVr82cYwkRW0nqE6bZkfNTrGLULB8872ZFGpbSbrAft5mDlSnprLRwrEA0SsFUd4O2W64pcvcJENa/NY+vvXAyd9jaHb5v0RpxUyllLrAIFuLEFHeHwyBAlMgq54dtYWqYa8pyJNoUiwOt158qzOE6wnoburP4KA9c= cache: pip: true directories: - $HOME/.cache/pre-commit - $HOME/Library/Caches/Homebrew install: - python -m pip install tox "setuptools>=28.2" - python -m tox --notest # Pre-populate a virtualenv with dependencies script: - python -m tox after_failure: - echo "Here's a list of installed Python packages:" - pip list --format=columns - echo Dumping logs, because tests failed to succeed - | for log in `ls cherrypy/test/*.log` do echo Outputting $log cat $log done - py_log=/home/travis/build/cherrypy/cherrypy/.tox/python/log/python-0.log - echo Outputting python invocation log from $py_log - cat $py_log stages: - *stage_lint_name - *stage_test_priority_name - name: *stage_test_name <<: *condition_api_or_cron - name: *stage_test_osx_name <<: *condition_api_or_cron - name: *stage_deploy_name if: tag IS present ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/.yamllint0000644252176402575230000000036314424762003016066 0ustar00jaracoprimarygroup--- extends: default rules: indentation: indent-sequences: false level: error truthy: allowed-values: - 'false' - 'no' - 'on' # Allow "on" key name in GHA CI/CD workflow definitions - 'true' - 'yes' ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/.zuul.yaml0000644252176402575230000000262314307416152016177 0ustar00jaracoprimarygroup--- - job: name: tox-build-docs parent: tox vars: tox_envlist: build-docs - job: name: tox-dist-check parent: tox vars: tox_envlist: dist-check - job: name: tox-pre-commit parent: tox vars: tox_envlist: pre-commit - job: name: tox-pre-commit-pep257 parent: tox voting: false vars: tox_envlist: pre-commit-pep257 - job: name: tox-py27-setup-check vars: tox_envlist: setup-check python_version: 2.7 python_use_pyenv: false parent: tox - job: name: tox-py35-setup-check vars: tox_envlist: setup-check python_version: 3.5 python_use_pyenv: true parent: tox - job: name: tox-py36-setup-check vars: tox_envlist: setup-check python_version: 3.6 python_use_pyenv: true parent: tox - job: name: tox-py37-setup-check vars: tox_envlist: setup-check python_version: 3.7 python_use_pyenv: false parent: tox - job: name: tox-py38-setup-check vars: tox_envlist: setup-check python_version: 3.8 python_use_pyenv: true parent: tox - project: check: jobs: - tox-build-docs - tox-dist-check - tox-pre-commit - tox-pre-commit-pep257 - tox-py27-setup-check - tox-py35-setup-check - tox-py36-setup-check - tox-py37-setup-check - tox-py38-setup-check ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477690.0 CherryPy-18.9.0/CHANGES.rst0000644252176402575230000005417614536337572016066 0ustar00jaracoprimarygroupv18.9.0 ------- * Various `changes `_. v18.8.0 ------- * :issue:`1974`: Dangerous characters received in a host header encoded using RFC 2047 are now elided by default. Currently, dangerous characters are defined as CR and LF. The original value is still available as ``cherrypy.request.headers['Host'].raw`` if needed. v18.7.0 ------- * :pr:`1923`: Drop support for Python 3.5. * :pr:`1945`: Fixed compatibility on Python 3.11. v18.6.1 ------- * :issue:`1849` via :pr:`1879`: Fixed XLF flag in gzip header emitted by gzip compression tool per :rfc:`1952#section-2.3.1` -- by :user:`webknjaz`. * :issue:`1874`: Restricted depending on pywin32 only under CPython so that it won't get pulled-in under PyPy -- by :user:`webknjaz`. * :issue:`1920`: Bumped minimum version of PyWin32 to 227. Block pywin32 install on Python 3.10 and later. v18.6.0 ------- * :issue:`1776` via :pr:`1851`: Add support for UTF-8 encoded attachment file names in ``Content-Disposition`` header via :rfc:`6266#appendix-D`. v18.5.0 ------- * :issue:`1827`: Fixed issue where bytes values in a ``HeaderMap`` would be converted to strings. * :pr:`1826`: Rely on `jaraco.collections `_ for its case-insensitive dictionary support. v18.4.0 ------- * :pr:`1715`: Fixed issue in cpstats where the ``data/`` endpoint would fail with encoding errors on Python 3. * :pr:`1821`: Simplify the passthrough of parameters to ``CPWebCase.getPage`` to cheroot. CherryPy now requires cheroot 8.2.1 or later. v18.3.0 ------- * :pr:`1806`: Support handling multiple exceptions when processing hooks as reported in :issue:`1770`. v18.2.0 ------- * File-based sessions no longer attempt to remove the lock files when releasing locks, instead deferring to the default behavior of zc.lockfile. Fixes :issue:`1391` and :issue:`1779`. * :pr:`1794`: Add native support for ``308 Permanent Redirect`` usable via ``raise cherrypy.HTTPRedirect('/new_uri', 308)``. v18.1.2 ------- * Fixed :issue:`1377` via :pr:`1785`: Restore a native WSGI-less HTTP server support. * :pr:`1769`: Reduce log level for non-error events in win32.py v18.1.1 ------- * :pr:`1774` reverts :pr:`1759` as new evidence emerged that the original behavior was intentional. Re-opens :issue:`1758`. v18.1.0 ------- * :issue:`1758` via :pr:`1759`: In the bus, when awaiting a state change, only publish after the state has changed. v18.0.1 ------- * :issue:`1738` via :pr:`1736`: Restore support for 'bytes' in response headers. * Substantial removal of Python 2 compatibility code. v18.0.0 ------- * :issue:`1730`: Drop support for Python 2.7. CherryPy 17 will remain an LTS release for bug and security fixes. * Drop support for Python 3.4. v17.4.2 ------- * Fixed :issue:`1377` by backporting :pr:`1785` via :pr:`1786`: Restore a native WSGI-less HTTP server support. v17.4.1 ------- * :issue:`1738` via :pr:`1755`: Restore support for 'bytes' in response headers (backport from v18.0.1). v17.4.0 ------- * :commit:`a95e619f`: When setting Response Body, reject Unicode values, making behavior on Python 2 same as on Python 3. * Other inconsequential refactorings. v17.3.0 ------- * :issue:`1193` via :pr:`1729`: Rely on zc.lockfile for session concurrency support. v17.2.0 ------- * :issue:`1690` via :pr:`1692`: Prevent orphaned Event object in cached 304 response. v17.1.0 ------- * :issue:`1694` via :pr:`1695`: Add support for accepting uploaded files with non-ascii filenames per RFC 5987. v17.0.0 ------- * :issue:`1673`: CherryPy now allows namespace packages for its dependencies. Environments that cannot handle namespace packgaes like py2exe will need to add such support or pin to older CherryPy versions. v16.0.3 ------- * :issue:`1722`: Pinned the ``tempora`` dependency against version 1.13 to avoid pulling in namespace packages. v16.0.2 ------- * :issue:`1716` via :pr:`1717`: Fixed handling of url-encoded parameters in digest authentication handling, correcting regression in v14.2.0. * :issue:`1719` via :commit:`1d41828`: Digest-auth tool will now return a status code of 401 for when a scheme other than 'digest' is indicated. v16.0.0 ------- * :issue:`1688` via :commit:`38ad1da`: Removed ``basic_auth`` and ``digest_auth`` tools and the ``httpauth`` module, which have been officially deprecated earlier in v14.0.0. * Removed deprecated properties: - ``cherrypy._cpreqbody.Entity.type`` deprecated in favor of :py:attr:`cherrypy._cpreqbody.Entity.content_type` - ``cherrypy._cprequest.Request.body_params`` deprecated in favor of :py:attr:`cherrypy._cprequest.RequestBody.params` * :issue:`1377`: In _cp_native server, set ``req.status`` using bytes (fixed in :pr:`1712`). * :issue:`1697` via :commit:`841f795`: Fixed error on Python 3.7 with AutoReloader when ``__file__`` is ``None``. * :issue:`1713` via :commit:`15aa80d`: Fix warning emitted during test run. * :issue:`1370` via :commit:`38f199c`: Fail with HTTP 400 for invalid headers. v15.0.0 ------- * :issue:`1708`: Removed components from webtest that were removed in the refactoring of cheroot.test.webtest for cheroot 6.1.0. v14.2.0 ------- * :issue:`1680` via :pr:`1683`: Basic Auth and Digest Auth tools now support :rfc:`7617` UTF-8 charset decoding where possible, using latin-1 as a fallback. v14.1.0 ------- * :cr-pr:`37`: Add support for peercreds lookup over UNIX domain socket. This enables app to automatically identify "who's on the other end of the wire". This is how you enable it:: server.peercreds: True server.peercreds_resolve: True The first option will put remote numeric data to WSGI env vars: app's PID, user's id and group. Second option will resolve that into user and group names. To prevent expensive syscalls, data is cached on per connection basis. v14.0.1 ------- * :issue:`1700`: Improve windows pywin32 dependency declaration via conditional extras. v14.0.0 ------- * :issue:`1688`: Officially deprecated ``basic_auth`` and ``digest_auth`` tools and the ``httpauth`` module, triggering DeprecationWarnings if they're used. Applications should instead adapt to use the more recent ``auth_basic`` and ``auth_digest`` tools. This deprecated functionality will be removed in a subsequent release soon. * Removed ``DeprecatedTool`` and the long-deprecated and disabled ``tidy`` and ``nsgmls`` tools. See `the rationale `_ for this change. v13.1.0 ------- * :issue:`1231` via :pr:`1654`: CaseInsensitiveDict now re-uses the generalized functionality from ``jaraco.collections`` to provide a more complete interface for a CaseInsensitiveDict and HeaderMap. Users are encouraged to use the implementation from `jaraco.collections `_ except when dealing with headers in CherryPy. v13.0.1 ------- * :pr:`1671`: Restore support for installing CherryPy into environments hostile to namespace packages, broken since the 11.1.0 release. v13.0.0 ------- * :issue:`1666`: Drop support for Python 3.3. v12.0.2 ------- * :issue:`1665`: In request processing, when an invalid cookie is received, render the actual error message reported rather than guessing (sometimes incorrectly) what error occurred. v12.0.1 ------- * Fixed issues importing :py:mod:`cherrypy.test.webtest` (by creating a module and importing classes from :py:mod:`cheroot`) and added a corresponding :py:class:`DeprecationWarning`. v12.0.0 ------- * Drop support for Python 3.1 and 3.2. * :issue:`1625`: Removed response timeout and timeout monitor and related exceptions, as it not possible to interrupt a request. Servers that wish to exit a request prematurely are recommended to monitor ``response.time`` and raise an exception or otherwise act accordingly. Servers that previously disabled timeouts by invoking ``cherrypy.engine.timeout_monitor.unsubscribe()`` will now crash. For forward-compatibility with this release on older versions of CherryPy, disable timeouts using the config option:: 'engine.timeout_monitor.on': False, Or test for the presence of the timeout_monitor attribute:: with contextlib2.suppress(AttributeError): cherrypy.engine.timeout_monitor.unsubscribe() Additionally, the ``TimeoutError`` exception has been removed, as it's no longer called anywhere. If your application benefits from this Exception, please comment in the linked ticket describing the use case, and we'll help devise a solution or bring the exception back. v11.3.0 ------- * Bump to cheroot 5.9.0. * ``cherrypy.test.webtest`` module is now merged with the ``cheroot.test.webtest`` module. The CherryPy name is retained for now for compatibility and will be removed eventually. v11.2.0 ------- * ``cherrypy.engine.subscribe`` now may be called without a callback, in which case it returns a decorator expecting the callback. * :pr:`1656`: Images are now compressed using lossless compression and consume less space. v11.1.0 ------- * :pr:`1611`: Expose default status logic for a redirect as ``HTTPRedirect.default_status``. * :pr:`1615`: ``HTTPRedirect.status`` is now an instance property and derived from the value in ``args``. Although it was previously possible to set the property on an instance, and this change prevents that possibilty, CherryPy never relied on that behavior and we presume no applications depend on that interface. * :issue:`1627`: Fixed issue in proxy tool where more than one port would appear in the ``request.base`` and thus in ``cherrypy.url``. * :pr:`1645`: Added new log format markers: - ``i`` holds a per-request UUID4 - ``z`` outputs UTC time in format of RFC 3339 - ``cherrypy._cprequest.Request.unique_id.uuid4`` now has lazily invocable UUID4 * :issue:`1646`: Improve http status conversion helper. * :pr:`1638`: Always use backslash for path separator when processing paths in staticdir. * :issue:`1190`: Fix gzip, caching, and staticdir tools integration. Makes cache of gzipped content valid. * Requires cheroot 5.8.3 or later. * Also, many improvements around continuous integration and code quality checks. This release contained an unintentional regression in environments that are hostile to namespace packages, such as Pex, Celery, and py2exe. See :pr:`1671` for details. v11.0.0 ------- * :issue:`1607`: Dropped support for Python 2.6. v10.2.2 ------- * :issue:`1595`: Fixed over-eager normalization of paths in cherrypy.url. v10.2.1 ------- * Remove unintended dependency on ``graphviz`` in Python 2.6. v10.2.0 ------- * :pr:`1580`: ``CPWSGIServer.version`` now reported as ``CherryPy/x.y.z Cheroot/x.y.z``. Bump to cheroot 5.2.0. * The codebase is now :pep:`8` complaint, flake8 linter is `enabled in TravisCI by default `_. * Max line restriction is now set to 120 for flake8 linter. * :pep:`257` linter runs as separate allowed failure job in Travis CI. * A few bugs related to undeclared variables have been fixed. * ``pre-commit`` testing goes faster due to enabled caching. v10.1.1 ------- * :issue:`1342`: Fix AssertionError on shutdown. v10.1.0 ------- * Bump to cheroot 5.1.0. * :issue:`794`: Prefer setting max-age for session cookie expiration, moving MSIE hack into a function documenting its purpose. v10.0.0 ------- * :issue:`1332`: CherryPy now uses `portend `_ for checking and waiting on ports for startup and teardown checks. The following names are no longer present: - cherrypy._cpserver.client_host - cherrypy._cpserver.check_port - cherrypy._cpserver.wait_for_free_port - cherrypy._cpserver.wait_for_occupied_port - cherrypy.process.servers.check_port - cherrypy.process.servers.wait_for_free_port - cherrypy.process.servers.wait_for_occupied_port Use this functionality from the portend package directly. v9.0.0 ------ * :issue:`1481`: Move functionality from cherrypy.wsgiserver to the `cheroot 5.0 `_ project. v8.9.1 ------ * :issue:`1537`: Restore dependency on pywin32 for Python 3.6. v8.9.0 ------ * :pr:`1547`: Replaced ``cherryd`` distutils script with a setuptools console entry point. When running CherryPy in daemon mode, the forked process no longer changes directory to ``/``. If that behavior is something on which your application relied and should rely, please file a ticket with the project. v8.8.0 ------ * :pr:`1528`: Allow a timeout of 0 to server. v8.7.0 ------ * :issue:`645`: Setting a bind port of 0 will bind to an ephemeral port. v8.6.0 ------ * :issue:`1538` and :issue:`1090`: Removed cruft from the setup script and instead rely on `include_package_data `_ to ensure the relevant files are included in the package. Note, this change does cause LICENSE.md no longer to be included in the installed package. v8.5.0 ------ * The pyOpenSSL support is now included on Python 3 builds, removing the last disparity between Python 2 and Python 3 in the CherryPy package. This change is one small step in consideration of :issue:`1399`. This change also fixes RPM builds, as reported in :issue:`1149`. v8.4.0 ------ * :issue:`1532`: Also release wheels for Python 2, enabling offline installation. v8.3.1 ------ * :issue:`1537`: Disable dependency on pypiwin32 on Python 3.6 until a viable build of pypiwin32 can be made on that Python version. v8.3.0 ------ * Consolidated some documentation and include the more concise readme in the package long description, as found on PyPI. v8.2.0 ------ * :issue:`1463`: CherryPy tests are now run under pytest and invoked using tox. v8.1.3 ------ * :issue:`1530`: Fix the issue with TypeError being swallowed by decorated handlers. v8.1.2 ------ * :issue:`1508` v8.1.1 ------ * :issue:`1497`: Handle errors thrown by ``ssl_module: 'builtin'`` when client opens connection to HTTPS port using HTTP. * :issue:`1350`: Fix regression introduced in v6.1.0 where environment construction for WSGIGateway_u0 was passing one parameter and not two. * Other miscellaneous fixes. v8.1.0 ------ * :issue:`1473`: ``HTTPError`` now also works as a context manager. * :issue:`1487`: The sessions tool now accepts a ``storage_class`` parameter, which supersedes the new deprecated ``storage_type`` parameter. The ``storage_class`` should be the actual Session subclass to be used. * Releases now use ``setuptools_scm`` to track the release versions. Therefore, releases can be cut by simply tagging a commit in the repo. Versions numbers are now stored in exactly one place. v8.0.1 ------ * :issue:`1489` via :pr:`1493`: Additionally reject anything else that's not bytes. * :issue:`1492`: systemd socket activation. v8.0.0 ------ * :issue:`1483`: Remove Deprecated constructs: - ``cherrypy.lib.http`` module. - ``unrepr``, ``modules``, and ``attributes`` in ``cherrypy.lib``. * :pr:`1476`: Drop support for python-memcached<1.58 * :issue:`1401`: Handle NoSSLErrors. * :issue:`1489`: In ``wsgiserver.WSGIGateway.respond``, the application must now yield bytes and not text, as the spec requires. If text is received, it will now raise a ValueError instead of silently encoding using ISO-8859-1. * Removed unicode filename from the package, working around :gh:`pypa/pip#3894 ` and :gh:`pypa/setuptools#704 `. v7.1.0 ------ * :pr:`1458`: Implement systemd's socket activation mechanism for CherryPy servers, based on work sponsored by Endless Computers. Socket Activation allows one to setup a system so that systemd will sit on a port and start services 'on demand' (a little bit like inetd and xinetd used to do). v7.0.0 ------ Removed the long-deprecated backward compatibility for legacy config keys in the engine. Use the config for the namespaced-plugins instead: - autoreload_on -> autoreload.on - autoreload_frequency -> autoreload.frequency - autoreload_match -> autoreload.match - reload_files -> autoreload.files - deadlock_poll_frequency -> timeout_monitor.frequency v6.2.1 ------ * :issue:`1460`: Fix KeyError in Bus.publish when signal handlers set in config. v6.2.0 ------ * :issue:`1441`: Added tool to automatically convert request params based on type annotations (primarily in Python 3). For example:: @cherrypy.tools.params() def resource(self, limit: int): assert isinstance(limit, int) v6.1.1 ------ * Issue :issue:`1411`: Fix issue where autoreload fails when the host interpreter for CherryPy was launched using ``python -m``. v6.1.0 ------ * Combined wsgiserver2 and wsgiserver3 modules into a single module, ``cherrypy.wsgiserver``. v6.0.2 ------ * Issue :pr:`1445`: Correct additional typos. v6.0.1 ------ * Issue :issue:`1444`: Correct typos in ``@cherrypy.expose`` decorators. v6.0.0 ------ * Setuptools is now required to build CherryPy. Pure distutils installs are no longer supported. This change allows CherryPy to depend on other packages and re-use code from them. It's still possible to install pre-built CherryPy packages (wheels) using pip without Setuptools. * `six `_ is now a requirement and subsequent requirements will be declared in the project metadata. * :issue:`1440`: Back out changes from :pr:`1432` attempting to fix redirects with Unicode URLs, as it also had the unintended consequence of causing the 'Location' to be ``bytes`` on Python 3. * ``cherrypy.expose`` now works on classes. * ``cherrypy.config`` decorator is now used throughout the code internally. v5.6.0 ------ * ``@cherrypy.expose`` now will also set the exposed attribute on a class. * Rewrote all tutorials and internal usage to prefer the decorator usage of ``expose`` rather than setting the attribute explicitly. * Removed test-specific code from tutorials. v5.5.0 ------ * :issue:`1397`: Fix for filenames with semicolons and quote characters in filenames found in headers. * :issue:`1311`: Added decorator for registering tools. * :issue:`1194`: Use simpler encoding rules for SCRIPT_NAME and PATH_INFO environment variables in CherryPy Tree allowing non-latin characters to pass even when ``wsgi.version`` is not ``u.0``. * :issue:`1352`: Ensure that multipart fields are decoded even when cached in a file. v5.4.0 ------ * ``cherrypy.test.webtest.WebCase`` now honors a 'WEBTEST_INTERACTIVE' environment variable to disable interactive tests (still enabled by default). Set to '0' or 'false' or 'False' to disable interactive tests. * :issue:`1408`: Fix AttributeError when listiterator was accessed using the ``next`` attribute. * :issue:`748`: Removed ``cherrypy.lib.sessions.PostgresqlSession``. * :pr:`1432`: Fix errors with redirects to Unicode URLs. v5.3.0 ------ * :issue:`1202`: Add support for specifying a certificate authority when serving SSL using the built-in SSL support. * Use ssl.create_default_context when available. * :issue:`1392`: Catch platform-specific socket errors on OS X. * :issue:`1386`: Fix parsing of URIs containing ``://`` in the path part. v5.2.0 ------ * :issue:`1410`: Moved hosting to Github (`cherrypy/cherrypy `_). v5.1.0 ------ * Bugfix issue :issue:`1315` for ``test_HTTP11_pipelining`` test in Python 3.5 * Bugfix issue :issue:`1382` regarding the keyword arguments support for Python 3 on the config file. * Bugfix issue :issue:`1406` for ``test_2_KeyboardInterrupt`` test in Python 3.5. by monkey patching the HTTPRequest given a bug on CPython that is affecting the testsuite (https://bugs.python.org/issue23377). * Add additional parameter ``raise_subcls`` to the tests helpers `openURL` and ``CPWebCase.getPage`` to have finer control on which exceptions can be raised. * Add support for direct keywords on the calls (e.g. ``foo=bar``) on the config file under Python 3. * Add additional validation to determine if the process is running as a daemon on ``cherrypy.process.plugins.SignalHandler`` to allow the execution of the testsuite under CI tools. v5.0.1 ------ * Bugfix for NameError following :issue:`94`. v5.0.0 ------ * Removed deprecated support for ``ssl_certificate`` and ``ssl_private_key`` attributes and implicit construction of SSL adapter on Python 2 WSGI servers. * Default SSL Adapter on Python 2 is the builtin SSL adapter, matching Python 3 behavior. * Pull request :issue:`94`: In proxy tool, defer to Host header for resolving the base if no base is supplied. v4.0.0 ------ * Drop support for Python 2.5 and earlier. * No longer build Windows installers by default. v3.8.2 ------ * Pull Request :issue:`116`: Correct InternalServerError when null bytes in static file path. Now responds with 404 instead. v3.8.0 ------ * Pull Request :issue:`96`: Pass ``exc_info`` to logger as keyword rather than formatting the error and injecting into the message. v3.7.0 ------ * CherryPy daemon may now be invoked with ``python -m cherrypy`` in addition to the ``cherryd`` script. * Issue :issue:`1298`: Fix SSL handling on CPython 2.7 with builtin SSL module and pyOpenSSL 0.14. This change will break PyPy for now. * Several documentation fixes. v3.6.0 ------ * Fixed HTTP range headers for negative length larger than content size. * Disabled universal wheel generation as wsgiserver has Python duality. * Pull Request :issue:`42`: Correct TypeError in ``check_auth`` when encrypt is used. * Pull Request :issue:`59`: Correct signature of HandlerWrapperTool. * Pull Request :issue:`60`: Fix error in SessionAuth where login_screen was incorrectly used. * Issue :issue:`1077`: Support keyword-only arguments in dispatchers (Python 3). * Issue :issue:`1019`: Allow logging host name in the access log. * Pull Request :issue:`50`: Fixed race condition in session cleanup. v3.5.0 ------ * Issue :issue:`1301`: When the incoming queue is full, now reject additional connections. This functionality was added to CherryPy 3.0, but unintentionally lost in 3.1. v3.4.0 ------ * Miscellaneous quality improvements. v3.3.0 ------ CherryPy adopts semver. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5526454 CherryPy-18.9.0/CherryPy.egg-info/0000755252176402575230000000000014536340235017474 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477980.0 CherryPy-18.9.0/CherryPy.egg-info/PKG-INFO0000644252176402575230000002101414536340234020566 0ustar00jaracoprimarygroupMetadata-Version: 2.1 Name: CherryPy Version: 18.9.0 Summary: Object-Oriented HTTP framework Home-page: https://www.cherrypy.dev Author: CherryPy Team Author-email: team@cherrypy.dev Project-URL: CI: AppVeyor, https://ci.appveyor.com/project/cherrypy/cherrypy Project-URL: CI: Travis, https://travis-ci.org/cherrypy/cherrypy Project-URL: CI: Circle, https://circleci.com/gh/cherrypy/cherrypy Project-URL: CI: GitHub, https://github.com/cherrypy/cherrypy/actions Project-URL: Docs: RTD, https://docs.cherrypy.dev Project-URL: GitHub: issues, https://github.com/cherrypy/cherrypy/issues Project-URL: GitHub: repo, https://github.com/cherrypy/cherrypy Project-URL: Tidelift: funding, https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: Freely Distributable Classifier: Operating System :: OS Independent Classifier: Framework :: CherryPy Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: Jython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Requires-Python: >=3.6 License-File: LICENSE.md Requires-Dist: cheroot>=8.2.1 Requires-Dist: portend>=2.1.1 Requires-Dist: more_itertools Requires-Dist: zc.lockfile Requires-Dist: jaraco.collections Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Requires-Dist: docutils; extra == "docs" Requires-Dist: alabaster; extra == "docs" Requires-Dist: sphinxcontrib-apidoc>=0.3.0; extra == "docs" Requires-Dist: rst.linker>=1.11; extra == "docs" Requires-Dist: jaraco.packaging>=3.2; extra == "docs" Requires-Dist: setuptools; extra == "docs" Provides-Extra: json Requires-Dist: simplejson; extra == "json" Provides-Extra: routes-dispatcher Requires-Dist: routes>=2.3.1; extra == "routes-dispatcher" Provides-Extra: ssl Requires-Dist: pyOpenSSL; extra == "ssl" Provides-Extra: testing Requires-Dist: coverage; extra == "testing" Requires-Dist: codecov; extra == "testing" Requires-Dist: objgraph; extra == "testing" Requires-Dist: pytest>=5.3.5; extra == "testing" Requires-Dist: pytest-cov; extra == "testing" Requires-Dist: pytest-forked; extra == "testing" Requires-Dist: pytest-sugar; extra == "testing" Requires-Dist: path.py; extra == "testing" Requires-Dist: requests_toolbelt; extra == "testing" Requires-Dist: pytest-services>=2; extra == "testing" Requires-Dist: setuptools; extra == "testing" Provides-Extra: memcached-session Requires-Dist: python-memcached>=1.58; extra == "memcached-session" Provides-Extra: xcgi Requires-Dist: flup; extra == "xcgi" Requires-Dist: pywin32>=227; sys_platform == "win32" and implementation_name == "cpython" and python_version < "3.10" .. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg :target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md :alt: SWUbanner .. image:: https://img.shields.io/pypi/v/cherrypy.svg :target: https://pypi.org/project/cherrypy .. image:: https://tidelift.com/badges/package/pypi/CherryPy :target: https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=readme :alt: CherryPy is available as part of the Tidelift Subscription .. image:: https://img.shields.io/badge/Python%203%20only-pip%20install%20%22%3E%3D18.0.0%22-%234da45e.svg :target: https://python3statement.org/ .. image:: https://img.shields.io/badge/Python%203%20and%202-pip%20install%20%22%3C18.0.0%22-%2349a7e9.svg :target: https://python3statement.org/#sections40-timeline .. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest :target: https://docs.cherrypy.dev/en/latest/?badge=latest .. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg :target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy .. image:: https://img.shields.io/badge/Mailing%20list-cherrypy--users-orange.svg :target: https://groups.google.com/group/cherrypy-users .. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg :target: https://gitter.im/cherrypy/cherrypy .. image:: https://img.shields.io/travis/cherrypy/cherrypy/master.svg?label=Linux%20build%20%40%20Travis%20CI :target: https://travis-ci.org/cherrypy/cherrypy .. image:: https://circleci.com/gh/cherrypy/cherrypy/tree/master.svg?style=svg :target: https://circleci.com/gh/cherrypy/cherrypy/tree/master .. image:: https://img.shields.io/appveyor/ci/CherryPy/cherrypy/master.svg?label=Windows%20build%20%40%20Appveyor :target: https://ci.appveyor.com/project/CherryPy/cherrypy/branch/master .. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600 :target: https://pypi.org/project/cheroot .. image:: https://img.shields.io/pypi/pyversions/cherrypy.svg :target: https://pypi.org/project/cherrypy .. image:: https://badges.github.io/stability-badges/dist/stable.svg :target: https://github.com/badges/stability-badges :alt: stable .. image:: https://api.codacy.com/project/badge/Grade/48b11060b5d249dc86e52dac2be2c715 :target: https://www.codacy.com/app/webknjaz/cherrypy-upstream?utm_source=github.com&utm_medium=referral&utm_content=cherrypy/cherrypy&utm_campaign=Badge_Grade .. image:: https://codecov.io/gh/cherrypy/cherrypy/branch/master/graph/badge.svg :target: https://codecov.io/gh/cherrypy/cherrypy :alt: codecov Welcome to the GitHub repository of `CherryPy `_! CherryPy is a pythonic, object-oriented HTTP framework. 1. It allows building web applications in much the same way one would build any other object-oriented program. 2. This design results in more concise and readable code developed faster. It's all just properties and methods. 3. It is now more than ten years old and has proven fast and very stable. 4. It is being used in production by many sites, from the simplest to the most demanding. 5. And perhaps most importantly, it is fun to work with :-) Here's how easy it is to write "Hello World" in CherryPy: .. code:: python import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello World!" cherrypy.quickstart(HelloWorld()) And it continues to work that intuitively when systems grow, allowing for the Python object model to be dynamically presented as a website and/or API. While CherryPy is one of the easiest and most intuitive frameworks out there, the prerequisite for understanding the `CherryPy documentation `_ is that you have a general understanding of Python and web development. Additionally: - Tutorials are included in the repository: https://github.com/cherrypy/cherrypy/tree/master/cherrypy/tutorial - A general wiki at: https://github.com/cherrypy/cherrypy/wiki If the docs are insufficient to address your needs, the CherryPy community has several `avenues for support `_. For Enterprise -------------- CherryPy is available as part of the Tidelift Subscription. The CherryPy maintainers and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. Contributing ------------ Please follow the `contribution guidelines `_. And by all means, absorb the `Zen of CherryPy `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477980.0 CherryPy-18.9.0/CherryPy.egg-info/SOURCES.txt0000644252176402575230000001156014536340234021362 0ustar00jaracoprimarygroup.appveyor.yml .codecov.yml .flake8 .gitattributes .gitignore .pre-commit-config-pep257.yaml .pre-commit-config.yaml .readthedocs.yml .travis.yml .yamllint .zuul.yaml CHANGES.rst LICENSE.md README.rst SECURITY.md bindep.txt conftest.py pyproject.toml pytest.ini setup.cfg setup.py tox.ini .circleci/config.yml .github/CONTRIBUTING.rst .github/FUNDING.yml .github/ISSUE_TEMPLATE.md .github/PULL_REQUEST_TEMPLATE.md .github/SUPPORT.rst .github/config.yml .github/patchback.yml .github/workflows/ci-cd.yml .test-results/pytest/.gitignore CherryPy.egg-info/PKG-INFO CherryPy.egg-info/SOURCES.txt CherryPy.egg-info/dependency_links.txt CherryPy.egg-info/entry_points.txt CherryPy.egg-info/requires.txt CherryPy.egg-info/top_level.txt cherrypy/__init__.py cherrypy/__main__.py cherrypy/_cpchecker.py cherrypy/_cpcompat.py cherrypy/_cpconfig.py cherrypy/_cpdispatch.py cherrypy/_cperror.py cherrypy/_cplogging.py cherrypy/_cpmodpy.py cherrypy/_cpnative_server.py cherrypy/_cpreqbody.py cherrypy/_cprequest.py cherrypy/_cpserver.py cherrypy/_cptools.py cherrypy/_cptree.py cherrypy/_cpwsgi.py cherrypy/_cpwsgi_server.py cherrypy/_helper.py cherrypy/_json.py cherrypy/daemon.py cherrypy/favicon.ico cherrypy/lib/__init__.py cherrypy/lib/auth_basic.py cherrypy/lib/auth_digest.py cherrypy/lib/caching.py cherrypy/lib/covercp.py cherrypy/lib/cpstats.py cherrypy/lib/cptools.py cherrypy/lib/encoding.py cherrypy/lib/gctools.py cherrypy/lib/httputil.py cherrypy/lib/jsontools.py cherrypy/lib/locking.py cherrypy/lib/profiler.py cherrypy/lib/reprconf.py cherrypy/lib/sessions.py cherrypy/lib/static.py cherrypy/lib/xmlrpcutil.py cherrypy/process/__init__.py cherrypy/process/plugins.py cherrypy/process/servers.py cherrypy/process/win32.py cherrypy/process/wspbus.py cherrypy/scaffold/__init__.py cherrypy/scaffold/apache-fcgi.conf cherrypy/scaffold/example.conf cherrypy/scaffold/site.conf cherrypy/scaffold/static/made_with_cherrypy_small.png cherrypy/test/__init__.py cherrypy/test/_test_decorators.py cherrypy/test/_test_states_demo.py cherrypy/test/benchmark.py cherrypy/test/checkerdemo.py cherrypy/test/fastcgi.conf cherrypy/test/fcgi.conf cherrypy/test/helper.py cherrypy/test/logtest.py cherrypy/test/modfastcgi.py cherrypy/test/modfcgid.py cherrypy/test/modpy.py cherrypy/test/modwsgi.py cherrypy/test/sessiondemo.py cherrypy/test/style.css cherrypy/test/test.pem cherrypy/test/test_auth_basic.py cherrypy/test/test_auth_digest.py cherrypy/test/test_bus.py cherrypy/test/test_caching.py cherrypy/test/test_config.py cherrypy/test/test_config_server.py cherrypy/test/test_conn.py cherrypy/test/test_core.py cherrypy/test/test_dynamicobjectmapping.py cherrypy/test/test_encoding.py cherrypy/test/test_etags.py cherrypy/test/test_http.py cherrypy/test/test_httputil.py cherrypy/test/test_iterator.py cherrypy/test/test_json.py cherrypy/test/test_logging.py cherrypy/test/test_mime.py cherrypy/test/test_misc_tools.py cherrypy/test/test_native.py cherrypy/test/test_objectmapping.py cherrypy/test/test_params.py cherrypy/test/test_plugins.py cherrypy/test/test_proxy.py cherrypy/test/test_refleaks.py cherrypy/test/test_request_obj.py cherrypy/test/test_routes.py cherrypy/test/test_session.py cherrypy/test/test_sessionauthenticate.py cherrypy/test/test_states.py cherrypy/test/test_static.py cherrypy/test/test_tools.py cherrypy/test/test_tutorials.py cherrypy/test/test_virtualhost.py cherrypy/test/test_wsgi_ns.py cherrypy/test/test_wsgi_unix_socket.py cherrypy/test/test_wsgi_vhost.py cherrypy/test/test_wsgiapps.py cherrypy/test/test_xmlrpc.py cherrypy/test/webtest.py cherrypy/test/static/404.html cherrypy/test/static/dirback.jpg cherrypy/test/static/index.html cherrypy/tutorial/README.rst cherrypy/tutorial/__init__.py cherrypy/tutorial/custom_error.html cherrypy/tutorial/pdf_file.pdf cherrypy/tutorial/tut01_helloworld.py cherrypy/tutorial/tut02_expose_methods.py cherrypy/tutorial/tut03_get_and_post.py cherrypy/tutorial/tut04_complex_site.py cherrypy/tutorial/tut05_derived_objects.py cherrypy/tutorial/tut06_default_method.py cherrypy/tutorial/tut07_sessions.py cherrypy/tutorial/tut08_generators_and_yield.py cherrypy/tutorial/tut09_files.py cherrypy/tutorial/tut10_http_errors.py cherrypy/tutorial/tutorial.conf docs/.gitignore docs/advanced.rst docs/basics.rst docs/conf.py docs/config.rst docs/contribute.rst docs/deploy.rst docs/development.rst docs/enterprise.rst docs/extend.rst docs/glossary.rst docs/history.rst docs/index.rst docs/install.rst docs/intro.rst docs/support.rst docs/tutorials.rst docs/_static/bgsides.png docs/_static/cpdocmain.css docs/_static/images/cherrypy_logo_big.png docs/_static/images/cpreturn.gif docs/_static/images/cpyield.gif docs/_static/images/sushibelt.JPG docs/_templates/python_2_eol.html docs/pkg/.gitignore man/cherryd.1 requirements/tox-build-dists.in tests/dist-check.py visuals/cherrypy_logo_big.png visuals/cherrypy_logo_small.jpg visuals/favicon.ico visuals/made_with_cherrypy_big.png visuals/made_with_cherrypy_small.png././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477980.0 CherryPy-18.9.0/CherryPy.egg-info/dependency_links.txt0000644252176402575230000000000114536340234023541 0ustar00jaracoprimarygroup ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477980.0 CherryPy-18.9.0/CherryPy.egg-info/entry_points.txt0000644252176402575230000000006214536340234022767 0ustar00jaracoprimarygroup[console_scripts] cherryd = cherrypy.__main__:run ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477980.0 CherryPy-18.9.0/CherryPy.egg-info/requires.txt0000644252176402575230000000106714536340234022077 0ustar00jaracoprimarygroupcheroot>=8.2.1 portend>=2.1.1 more_itertools zc.lockfile jaraco.collections [:sys_platform == "win32" and implementation_name == "cpython" and python_version < "3.10"] pywin32>=227 [docs] sphinx docutils alabaster sphinxcontrib-apidoc>=0.3.0 rst.linker>=1.11 jaraco.packaging>=3.2 setuptools [json] simplejson [memcached_session] python-memcached>=1.58 [routes_dispatcher] routes>=2.3.1 [ssl] pyOpenSSL [testing] coverage codecov objgraph pytest>=5.3.5 pytest-cov pytest-forked pytest-sugar path.py requests_toolbelt pytest-services>=2 setuptools [xcgi] flup ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477980.0 CherryPy-18.9.0/CherryPy.egg-info/top_level.txt0000644252176402575230000000001114536340234022215 0ustar00jaracoprimarygroupcherrypy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/LICENSE.md0000644252176402575230000000274714307416152015651 0ustar00jaracoprimarygroupCopyright © 2004-2019, CherryPy Team (team@cherrypy.dev) All rights reserved. * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of CherryPy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5544581 CherryPy-18.9.0/PKG-INFO0000644252176402575230000002101414536340235015330 0ustar00jaracoprimarygroupMetadata-Version: 2.1 Name: CherryPy Version: 18.9.0 Summary: Object-Oriented HTTP framework Home-page: https://www.cherrypy.dev Author: CherryPy Team Author-email: team@cherrypy.dev Project-URL: CI: AppVeyor, https://ci.appveyor.com/project/cherrypy/cherrypy Project-URL: CI: Travis, https://travis-ci.org/cherrypy/cherrypy Project-URL: CI: Circle, https://circleci.com/gh/cherrypy/cherrypy Project-URL: CI: GitHub, https://github.com/cherrypy/cherrypy/actions Project-URL: Docs: RTD, https://docs.cherrypy.dev Project-URL: GitHub: issues, https://github.com/cherrypy/cherrypy/issues Project-URL: GitHub: repo, https://github.com/cherrypy/cherrypy Project-URL: Tidelift: funding, https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: Freely Distributable Classifier: Operating System :: OS Independent Classifier: Framework :: CherryPy Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: Jython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Requires-Python: >=3.6 License-File: LICENSE.md Requires-Dist: cheroot>=8.2.1 Requires-Dist: portend>=2.1.1 Requires-Dist: more_itertools Requires-Dist: zc.lockfile Requires-Dist: jaraco.collections Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Requires-Dist: docutils; extra == "docs" Requires-Dist: alabaster; extra == "docs" Requires-Dist: sphinxcontrib-apidoc>=0.3.0; extra == "docs" Requires-Dist: rst.linker>=1.11; extra == "docs" Requires-Dist: jaraco.packaging>=3.2; extra == "docs" Requires-Dist: setuptools; extra == "docs" Provides-Extra: json Requires-Dist: simplejson; extra == "json" Provides-Extra: routes-dispatcher Requires-Dist: routes>=2.3.1; extra == "routes-dispatcher" Provides-Extra: ssl Requires-Dist: pyOpenSSL; extra == "ssl" Provides-Extra: testing Requires-Dist: coverage; extra == "testing" Requires-Dist: codecov; extra == "testing" Requires-Dist: objgraph; extra == "testing" Requires-Dist: pytest>=5.3.5; extra == "testing" Requires-Dist: pytest-cov; extra == "testing" Requires-Dist: pytest-forked; extra == "testing" Requires-Dist: pytest-sugar; extra == "testing" Requires-Dist: path.py; extra == "testing" Requires-Dist: requests_toolbelt; extra == "testing" Requires-Dist: pytest-services>=2; extra == "testing" Requires-Dist: setuptools; extra == "testing" Provides-Extra: memcached-session Requires-Dist: python-memcached>=1.58; extra == "memcached-session" Provides-Extra: xcgi Requires-Dist: flup; extra == "xcgi" Requires-Dist: pywin32>=227; sys_platform == "win32" and implementation_name == "cpython" and python_version < "3.10" .. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg :target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md :alt: SWUbanner .. image:: https://img.shields.io/pypi/v/cherrypy.svg :target: https://pypi.org/project/cherrypy .. image:: https://tidelift.com/badges/package/pypi/CherryPy :target: https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=readme :alt: CherryPy is available as part of the Tidelift Subscription .. image:: https://img.shields.io/badge/Python%203%20only-pip%20install%20%22%3E%3D18.0.0%22-%234da45e.svg :target: https://python3statement.org/ .. image:: https://img.shields.io/badge/Python%203%20and%202-pip%20install%20%22%3C18.0.0%22-%2349a7e9.svg :target: https://python3statement.org/#sections40-timeline .. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest :target: https://docs.cherrypy.dev/en/latest/?badge=latest .. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg :target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy .. image:: https://img.shields.io/badge/Mailing%20list-cherrypy--users-orange.svg :target: https://groups.google.com/group/cherrypy-users .. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg :target: https://gitter.im/cherrypy/cherrypy .. image:: https://img.shields.io/travis/cherrypy/cherrypy/master.svg?label=Linux%20build%20%40%20Travis%20CI :target: https://travis-ci.org/cherrypy/cherrypy .. image:: https://circleci.com/gh/cherrypy/cherrypy/tree/master.svg?style=svg :target: https://circleci.com/gh/cherrypy/cherrypy/tree/master .. image:: https://img.shields.io/appveyor/ci/CherryPy/cherrypy/master.svg?label=Windows%20build%20%40%20Appveyor :target: https://ci.appveyor.com/project/CherryPy/cherrypy/branch/master .. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600 :target: https://pypi.org/project/cheroot .. image:: https://img.shields.io/pypi/pyversions/cherrypy.svg :target: https://pypi.org/project/cherrypy .. image:: https://badges.github.io/stability-badges/dist/stable.svg :target: https://github.com/badges/stability-badges :alt: stable .. image:: https://api.codacy.com/project/badge/Grade/48b11060b5d249dc86e52dac2be2c715 :target: https://www.codacy.com/app/webknjaz/cherrypy-upstream?utm_source=github.com&utm_medium=referral&utm_content=cherrypy/cherrypy&utm_campaign=Badge_Grade .. image:: https://codecov.io/gh/cherrypy/cherrypy/branch/master/graph/badge.svg :target: https://codecov.io/gh/cherrypy/cherrypy :alt: codecov Welcome to the GitHub repository of `CherryPy `_! CherryPy is a pythonic, object-oriented HTTP framework. 1. It allows building web applications in much the same way one would build any other object-oriented program. 2. This design results in more concise and readable code developed faster. It's all just properties and methods. 3. It is now more than ten years old and has proven fast and very stable. 4. It is being used in production by many sites, from the simplest to the most demanding. 5. And perhaps most importantly, it is fun to work with :-) Here's how easy it is to write "Hello World" in CherryPy: .. code:: python import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello World!" cherrypy.quickstart(HelloWorld()) And it continues to work that intuitively when systems grow, allowing for the Python object model to be dynamically presented as a website and/or API. While CherryPy is one of the easiest and most intuitive frameworks out there, the prerequisite for understanding the `CherryPy documentation `_ is that you have a general understanding of Python and web development. Additionally: - Tutorials are included in the repository: https://github.com/cherrypy/cherrypy/tree/master/cherrypy/tutorial - A general wiki at: https://github.com/cherrypy/cherrypy/wiki If the docs are insufficient to address your needs, the CherryPy community has several `avenues for support `_. For Enterprise -------------- CherryPy is available as part of the Tidelift Subscription. The CherryPy maintainers and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. Contributing ------------ Please follow the `contribution guidelines `_. And by all means, absorb the `Zen of CherryPy `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/README.rst0000644252176402575230000001150214307416152015721 0ustar00jaracoprimarygroup.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg :target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md :alt: SWUbanner .. image:: https://img.shields.io/pypi/v/cherrypy.svg :target: https://pypi.org/project/cherrypy .. image:: https://tidelift.com/badges/package/pypi/CherryPy :target: https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=readme :alt: CherryPy is available as part of the Tidelift Subscription .. image:: https://img.shields.io/badge/Python%203%20only-pip%20install%20%22%3E%3D18.0.0%22-%234da45e.svg :target: https://python3statement.org/ .. image:: https://img.shields.io/badge/Python%203%20and%202-pip%20install%20%22%3C18.0.0%22-%2349a7e9.svg :target: https://python3statement.org/#sections40-timeline .. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest :target: https://docs.cherrypy.dev/en/latest/?badge=latest .. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg :target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy .. image:: https://img.shields.io/badge/Mailing%20list-cherrypy--users-orange.svg :target: https://groups.google.com/group/cherrypy-users .. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg :target: https://gitter.im/cherrypy/cherrypy .. image:: https://img.shields.io/travis/cherrypy/cherrypy/master.svg?label=Linux%20build%20%40%20Travis%20CI :target: https://travis-ci.org/cherrypy/cherrypy .. image:: https://circleci.com/gh/cherrypy/cherrypy/tree/master.svg?style=svg :target: https://circleci.com/gh/cherrypy/cherrypy/tree/master .. image:: https://img.shields.io/appveyor/ci/CherryPy/cherrypy/master.svg?label=Windows%20build%20%40%20Appveyor :target: https://ci.appveyor.com/project/CherryPy/cherrypy/branch/master .. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600 :target: https://pypi.org/project/cheroot .. image:: https://img.shields.io/pypi/pyversions/cherrypy.svg :target: https://pypi.org/project/cherrypy .. image:: https://badges.github.io/stability-badges/dist/stable.svg :target: https://github.com/badges/stability-badges :alt: stable .. image:: https://api.codacy.com/project/badge/Grade/48b11060b5d249dc86e52dac2be2c715 :target: https://www.codacy.com/app/webknjaz/cherrypy-upstream?utm_source=github.com&utm_medium=referral&utm_content=cherrypy/cherrypy&utm_campaign=Badge_Grade .. image:: https://codecov.io/gh/cherrypy/cherrypy/branch/master/graph/badge.svg :target: https://codecov.io/gh/cherrypy/cherrypy :alt: codecov Welcome to the GitHub repository of `CherryPy `_! CherryPy is a pythonic, object-oriented HTTP framework. 1. It allows building web applications in much the same way one would build any other object-oriented program. 2. This design results in more concise and readable code developed faster. It's all just properties and methods. 3. It is now more than ten years old and has proven fast and very stable. 4. It is being used in production by many sites, from the simplest to the most demanding. 5. And perhaps most importantly, it is fun to work with :-) Here's how easy it is to write "Hello World" in CherryPy: .. code:: python import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello World!" cherrypy.quickstart(HelloWorld()) And it continues to work that intuitively when systems grow, allowing for the Python object model to be dynamically presented as a website and/or API. While CherryPy is one of the easiest and most intuitive frameworks out there, the prerequisite for understanding the `CherryPy documentation `_ is that you have a general understanding of Python and web development. Additionally: - Tutorials are included in the repository: https://github.com/cherrypy/cherrypy/tree/master/cherrypy/tutorial - A general wiki at: https://github.com/cherrypy/cherrypy/wiki If the docs are insufficient to address your needs, the CherryPy community has several `avenues for support `_. For Enterprise -------------- CherryPy is available as part of the Tidelift Subscription. The CherryPy maintainers and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. Contributing ------------ Please follow the `contribution guidelines `_. And by all means, absorb the `Zen of CherryPy `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1691156943.0 CherryPy-18.9.0/SECURITY.md0000644252176402575230000000026414463200717016027 0ustar00jaracoprimarygroup# Security Contact To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/bindep.txt0000644252176402575230000000017014307416152016233 0ustar00jaracoprimarygroupgcc [platform:dpkg] libenchant1c2a [platform:dpkg] python3-dev [platform:dpkg] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1702477980.523393 CherryPy-18.9.0/cherrypy/0000755252176402575230000000000014536340235016102 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/__init__.py0000644252176402575230000002602514307416152020216 0ustar00jaracoprimarygroup"""CherryPy is a pythonic, object-oriented HTTP framework. CherryPy consists of not one, but four separate API layers. The APPLICATION LAYER is the simplest. CherryPy applications are written as a tree of classes and methods, where each branch in the tree corresponds to a branch in the URL path. Each method is a 'page handler', which receives GET and POST params as keyword arguments, and returns or yields the (HTML) body of the response. The special method name 'index' is used for paths that end in a slash, and the special method name 'default' is used to handle multiple paths via a single handler. This layer also includes: * the 'exposed' attribute (and cherrypy.expose) * cherrypy.quickstart() * _cp_config attributes * cherrypy.tools (including cherrypy.session) * cherrypy.url() The ENVIRONMENT LAYER is used by developers at all levels. It provides information about the current request and response, plus the application and server environment, via a (default) set of top-level objects: * cherrypy.request * cherrypy.response * cherrypy.engine * cherrypy.server * cherrypy.tree * cherrypy.config * cherrypy.thread_data * cherrypy.log * cherrypy.HTTPError, NotFound, and HTTPRedirect * cherrypy.lib The EXTENSION LAYER allows advanced users to construct and share their own plugins. It consists of: * Hook API * Tool API * Toolbox API * Dispatch API * Config Namespace API Finally, there is the CORE LAYER, which uses the core API's to construct the default components which are available at higher layers. You can think of the default components as the 'reference implementation' for CherryPy. Megaframeworks (and advanced users) may replace the default components with customized or extended components. The core API's are: * Application API * Engine API * Request API * Server API * WSGI API These API's are described in the `CherryPy specification `_. """ try: import pkg_resources except ImportError: pass from threading import local as _local from ._cperror import ( HTTPError, HTTPRedirect, InternalRedirect, NotFound, CherryPyException, ) from . import _cpdispatch as dispatch from ._cptools import default_toolbox as tools, Tool from ._helper import expose, popargs, url from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig import cherrypy.lib.httputil as _httputil from ._cptree import Application from . import _cpwsgi as wsgi from . import process try: from .process import win32 engine = win32.Win32Bus() engine.console_control_handler = win32.ConsoleCtrlHandler(engine) del win32 except ImportError: engine = process.bus from . import _cpchecker __all__ = ( 'HTTPError', 'HTTPRedirect', 'InternalRedirect', 'NotFound', 'CherryPyException', 'dispatch', 'tools', 'Tool', 'Application', 'wsgi', 'process', 'tree', 'engine', 'quickstart', 'serving', 'request', 'response', 'thread_data', 'log', 'expose', 'popargs', 'url', 'config', ) __import__('cherrypy._cptools') __import__('cherrypy._cprequest') tree = _cptree.Tree() try: __version__ = pkg_resources.require('cherrypy')[0].version except Exception: __version__ = 'unknown' engine.listeners['before_request'] = set() engine.listeners['after_request'] = set() engine.autoreload = process.plugins.Autoreloader(engine) engine.autoreload.subscribe() engine.thread_manager = process.plugins.ThreadManager(engine) engine.thread_manager.subscribe() engine.signal_handler = process.plugins.SignalHandler(engine) class _HandleSignalsPlugin(object): """Handle signals from other processes. Based on the configured platform handlers above. """ def __init__(self, bus): self.bus = bus def subscribe(self): """Add the handlers based on the platform.""" if hasattr(self.bus, 'signal_handler'): self.bus.signal_handler.subscribe() if hasattr(self.bus, 'console_control_handler'): self.bus.console_control_handler.subscribe() engine.signals = _HandleSignalsPlugin(engine) server = _cpserver.Server() server.subscribe() def quickstart(root=None, script_name='', config=None): """Mount the given root, start the builtin server (and engine), then block. root: an instance of a "controller class" (a collection of page handler methods) which represents the root of the application. script_name: a string containing the "mount point" of the application. This should start with a slash, and be the path portion of the URL at which to mount the given root. For example, if root.index() will handle requests to "http://www.example.com:8080/dept/app1/", then the script_name argument would be "/dept/app1". It MUST NOT end in a slash. If the script_name refers to the root of the URI, it MUST be an empty string (not "/"). config: a file or dict containing application config. If this contains a [global] section, those entries will be used in the global (site-wide) config. """ if config: _global_conf_alias.update(config) tree.mount(root, script_name, config) engine.signals.subscribe() engine.start() engine.block() class _Serving(_local): """An interface for registering request and response objects. Rather than have a separate "thread local" object for the request and the response, this class works as a single threadlocal container for both objects (and any others which developers wish to define). In this way, we can easily dump those objects when we stop/start a new HTTP conversation, yet still refer to them as module-level globals in a thread-safe way. """ request = _cprequest.Request(_httputil.Host('127.0.0.1', 80), _httputil.Host('127.0.0.1', 1111)) """ The request object for the current thread. In the main thread, and any threads which are not receiving HTTP requests, this is None.""" response = _cprequest.Response() """ The response object for the current thread. In the main thread, and any threads which are not receiving HTTP requests, this is None.""" def load(self, request, response): self.request = request self.response = response def clear(self): """Remove all attributes of self.""" self.__dict__.clear() serving = _Serving() class _ThreadLocalProxy(object): __slots__ = ['__attrname__', '__dict__'] def __init__(self, attrname): self.__attrname__ = attrname def __getattr__(self, name): child = getattr(serving, self.__attrname__) return getattr(child, name) def __setattr__(self, name, value): if name in ('__attrname__', ): object.__setattr__(self, name, value) else: child = getattr(serving, self.__attrname__) setattr(child, name, value) def __delattr__(self, name): child = getattr(serving, self.__attrname__) delattr(child, name) @property def __dict__(self): child = getattr(serving, self.__attrname__) d = child.__class__.__dict__.copy() d.update(child.__dict__) return d def __getitem__(self, key): child = getattr(serving, self.__attrname__) return child[key] def __setitem__(self, key, value): child = getattr(serving, self.__attrname__) child[key] = value def __delitem__(self, key): child = getattr(serving, self.__attrname__) del child[key] def __contains__(self, key): child = getattr(serving, self.__attrname__) return key in child def __len__(self): child = getattr(serving, self.__attrname__) return len(child) def __nonzero__(self): child = getattr(serving, self.__attrname__) return bool(child) # Python 3 __bool__ = __nonzero__ # Create request and response object (the same objects will be used # throughout the entire life of the webserver, but will redirect # to the "serving" object) request = _ThreadLocalProxy('request') response = _ThreadLocalProxy('response') # Create thread_data object as a thread-specific all-purpose storage class _ThreadData(_local): """A container for thread-specific data.""" thread_data = _ThreadData() # Monkeypatch pydoc to allow help() to go through the threadlocal proxy. # Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. # The only other way would be to change what is returned from type(request) # and that's not possible in pure Python (you'd have to fake ob_type). def _cherrypy_pydoc_resolve(thing, forceload=0): """Given an object or a path to an object, get the object and its name.""" if isinstance(thing, _ThreadLocalProxy): thing = getattr(serving, thing.__attrname__) return _pydoc._builtin_resolve(thing, forceload) try: import pydoc as _pydoc _pydoc._builtin_resolve = _pydoc.resolve _pydoc.resolve = _cherrypy_pydoc_resolve except ImportError: pass class _GlobalLogManager(_cplogging.LogManager): """A site-wide LogManager; routes to app.log or global log as appropriate. This :class:`LogManager` implements cherrypy.log() and cherrypy.log.access(). If either function is called during a request, the message will be sent to the logger for the current Application. If they are called outside of a request, the message will be sent to the site-wide logger. """ def __call__(self, *args, **kwargs): """Log the given message to the app.log or global log. Log the given message to the app.log or global log as appropriate. """ # Do NOT use try/except here. See # https://github.com/cherrypy/cherrypy/issues/945 if hasattr(request, 'app') and hasattr(request.app, 'log'): log = request.app.log else: log = self return log.error(*args, **kwargs) def access(self): """Log an access message to the app.log or global log. Log the given message to the app.log or global log as appropriate. """ try: return request.app.log.access() except AttributeError: return _cplogging.LogManager.access(self) log = _GlobalLogManager() # Set a default screen handler on the global log. log.screen = True log.error_file = '' # Using an access file makes CP about 10% slower. Leave off by default. log.access_file = '' @engine.subscribe('log') def _buslog(msg, level): log.error(msg, 'ENGINE', severity=level) # Use _global_conf_alias so quickstart can use 'config' as an arg # without shadowing cherrypy.config. config = _global_conf_alias = _cpconfig.Config() config.defaults = { 'tools.log_tracebacks.on': True, 'tools.log_headers.on': True, 'tools.trailing_slash.on': True, 'tools.encode.on': True } config.namespaces['log'] = lambda k, v: setattr(log, k, v) config.namespaces['checker'] = lambda k, v: setattr(checker, k, v) # Must reset to get our defaults applied. config.reset() checker = _cpchecker.Checker() engine.subscribe('start', checker) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/__main__.py0000755252176402575230000000015314307416152020174 0ustar00jaracoprimarygroup"""CherryPy'd cherryd daemon runner.""" from cherrypy.daemon import run __name__ == '__main__' and run() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpchecker.py0000644252176402575230000003434614307416152020552 0ustar00jaracoprimarygroup"""Checker for CherryPy sites and mounted apps.""" import os import warnings import builtins import cherrypy class Checker(object): """A checker for CherryPy sites and their mounted applications. When this object is called at engine startup, it executes each of its own methods whose names start with ``check_``. If you wish to disable selected checks, simply add a line in your global config which sets the appropriate method to False:: [global] checker.check_skipped_app_config = False You may also dynamically add or replace ``check_*`` methods in this way. """ on = True """If True (the default), run all checks; if False, turn off all checks.""" def __init__(self): """Initialize Checker instance.""" self._populate_known_types() def __call__(self): """Run all check_* methods.""" if self.on: oldformatwarning = warnings.formatwarning warnings.formatwarning = self.formatwarning try: for name in dir(self): if name.startswith('check_'): method = getattr(self, name) if method and hasattr(method, '__call__'): method() finally: warnings.formatwarning = oldformatwarning def formatwarning(self, message, category, filename, lineno, line=None): """Format a warning.""" return 'CherryPy Checker:\n%s\n\n' % message # This value should be set inside _cpconfig. global_config_contained_paths = False def check_app_config_entries_dont_start_with_script_name(self): """Check for App config with sections that repeat script_name.""" for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue if not app.config: continue if sn == '': continue sn_atoms = sn.strip('/').split('/') for key in app.config.keys(): key_atoms = key.strip('/').split('/') if key_atoms[:len(sn_atoms)] == sn_atoms: warnings.warn( 'The application mounted at %r has config ' 'entries that start with its script name: %r' % (sn, key)) def check_site_config_entries_in_app_config(self): """Check for mounted Applications that have site-scoped config.""" for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue msg = [] for section, entries in app.config.items(): if section.startswith('/'): for key, value in entries.items(): for n in ('engine.', 'server.', 'tree.', 'checker.'): if key.startswith(n): msg.append('[%s] %s = %s' % (section, key, value)) if msg: msg.insert(0, 'The application mounted at %r contains the ' 'following config entries, which are only allowed ' 'in site-wide config. Move them to a [global] ' 'section and pass them to cherrypy.config.update() ' 'instead of tree.mount().' % sn) warnings.warn(os.linesep.join(msg)) def check_skipped_app_config(self): """Check for mounted Applications that have no config.""" for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue if not app.config: msg = 'The Application mounted at %r has an empty config.' % sn if self.global_config_contained_paths: msg += (' It looks like the config you passed to ' 'cherrypy.config.update() contains application-' 'specific sections. You must explicitly pass ' 'application config via ' 'cherrypy.tree.mount(..., config=app_config)') warnings.warn(msg) return def check_app_config_brackets(self): """Check for App config with extraneous brackets in section names.""" for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue if not app.config: continue for key in app.config.keys(): if key.startswith('[') or key.endswith(']'): warnings.warn( 'The application mounted at %r has config ' 'section names with extraneous brackets: %r. ' 'Config *files* need brackets; config *dicts* ' '(e.g. passed to tree.mount) do not.' % (sn, key)) def check_static_paths(self): """Check Application config for incorrect static paths.""" # Use the dummy Request object in the main thread. request = cherrypy.request for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue request.app = app for section in app.config: # get_resource will populate request.config request.get_resource(section + '/dummy.html') conf = request.config.get if conf('tools.staticdir.on', False): msg = '' root = conf('tools.staticdir.root') dir = conf('tools.staticdir.dir') if dir is None: msg = 'tools.staticdir.dir is not set.' else: fulldir = '' if os.path.isabs(dir): fulldir = dir if root: msg = ('dir is an absolute path, even ' 'though a root is provided.') testdir = os.path.join(root, dir[1:]) if os.path.exists(testdir): msg += ( '\nIf you meant to serve the ' 'filesystem folder at %r, remove the ' 'leading slash from dir.' % (testdir,)) else: if not root: msg = ( 'dir is a relative path and ' 'no root provided.') else: fulldir = os.path.join(root, dir) if not os.path.isabs(fulldir): msg = ('%r is not an absolute path.' % ( fulldir,)) if fulldir and not os.path.exists(fulldir): if msg: msg += '\n' msg += ('%r (root + dir) is not an existing ' 'filesystem path.' % fulldir) if msg: warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r' % (msg, section, root, dir)) # -------------------------- Compatibility -------------------------- # obsolete = { 'server.default_content_type': 'tools.response_headers.headers', 'log_access_file': 'log.access_file', 'log_config_options': None, 'log_file': 'log.error_file', 'log_file_not_found': None, 'log_request_headers': 'tools.log_headers.on', 'log_to_screen': 'log.screen', 'show_tracebacks': 'request.show_tracebacks', 'throw_errors': 'request.throw_errors', 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' 'cherrypy.Application(Root())))'), } deprecated = {} def _compat(self, config): """Process config and warn on each obsolete or deprecated entry.""" for section, conf in config.items(): if isinstance(conf, dict): for k in conf: if k in self.obsolete: warnings.warn('%r is obsolete. Use %r instead.\n' 'section: [%s]' % (k, self.obsolete[k], section)) elif k in self.deprecated: warnings.warn('%r is deprecated. Use %r instead.\n' 'section: [%s]' % (k, self.deprecated[k], section)) else: if section in self.obsolete: warnings.warn('%r is obsolete. Use %r instead.' % (section, self.obsolete[section])) elif section in self.deprecated: warnings.warn('%r is deprecated. Use %r instead.' % (section, self.deprecated[section])) def check_compatibility(self): """Process config and warn on each obsolete or deprecated entry.""" self._compat(cherrypy.config) for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue self._compat(app.config) # ------------------------ Known Namespaces ------------------------ # extra_config_namespaces = [] def _known_ns(self, app): ns = ['wsgi'] ns.extend(app.toolboxes) ns.extend(app.namespaces) ns.extend(app.request_class.namespaces) ns.extend(cherrypy.config.namespaces) ns += self.extra_config_namespaces for section, conf in app.config.items(): is_path_section = section.startswith('/') if is_path_section and isinstance(conf, dict): for k in conf: atoms = k.split('.') if len(atoms) > 1: if atoms[0] not in ns: # Spit out a special warning if a known # namespace is preceded by "cherrypy." if atoms[0] == 'cherrypy' and atoms[1] in ns: msg = ( 'The config entry %r is invalid; ' 'try %r instead.\nsection: [%s]' % (k, '.'.join(atoms[1:]), section)) else: msg = ( 'The config entry %r is invalid, ' 'because the %r config namespace ' 'is unknown.\n' 'section: [%s]' % (k, atoms[0], section)) warnings.warn(msg) elif atoms[0] == 'tools': if atoms[1] not in dir(cherrypy.tools): msg = ( 'The config entry %r may be invalid, ' 'because the %r tool was not found.\n' 'section: [%s]' % (k, atoms[1], section)) warnings.warn(msg) def check_config_namespaces(self): """Process config and warn on each unknown config namespace.""" for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue self._known_ns(app) # -------------------------- Config Types -------------------------- # known_config_types = {} def _populate_known_types(self): b = [x for x in vars(builtins).values() if type(x) is type(str)] def traverse(obj, namespace): for name in dir(obj): # Hack for 3.2's warning about body_params if name == 'body_params': continue vtype = type(getattr(obj, name, None)) if vtype in b: self.known_config_types[namespace + '.' + name] = vtype traverse(cherrypy.request, 'request') traverse(cherrypy.response, 'response') traverse(cherrypy.server, 'server') traverse(cherrypy.engine, 'engine') traverse(cherrypy.log, 'log') def _known_types(self, config): msg = ('The config entry %r in section %r is of type %r, ' 'which does not match the expected type %r.') for section, conf in config.items(): if not isinstance(conf, dict): conf = {section: conf} for k, v in conf.items(): if v is not None: expected_type = self.known_config_types.get(k, None) vtype = type(v) if expected_type and vtype != expected_type: warnings.warn(msg % (k, section, vtype.__name__, expected_type.__name__)) def check_config_types(self): """Assert that config values are of the same type as default values.""" self._known_types(cherrypy.config) for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue self._known_types(app.config) # -------------------- Specific config warnings -------------------- # def check_localhost(self): """Warn if any socket_host is 'localhost'. See #711.""" for k, v in cherrypy.config.items(): if k == 'server.socket_host' and v == 'localhost': warnings.warn("The use of 'localhost' as a socket host can " 'cause problems on newer systems, since ' "'localhost' can map to either an IPv4 or an " "IPv6 address. You should use '127.0.0.1' " "or '[::1]' instead.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpcompat.py0000644252176402575230000000371014307416152020420 0ustar00jaracoprimarygroup"""Compatibility code for using CherryPy with various versions of Python. To retain compatibility with older Python versions, this module provides a useful abstraction over the differences between Python versions, sometimes by preferring a newer idiom, sometimes an older one, and sometimes a custom one. In particular, Python 2 uses str and '' for byte strings, while Python 3 uses str and '' for unicode strings. We will call each of these the 'native string' type for each version. Because of this major difference, this module provides two functions: 'ntob', which translates native strings (of type 'str') into byte strings regardless of Python version, and 'ntou', which translates native strings to unicode strings. Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'. They were created with Python 2.3-2.5 compatibility in mind. Instead, use unicode literals (from __future__) and bytes literals and their .encode/.decode methods as needed. """ import http.client def ntob(n, encoding='ISO-8859-1'): """Return the given native string as a byte string in the given encoding. """ assert_native(n) # In Python 3, the native string type is unicode return n.encode(encoding) def ntou(n, encoding='ISO-8859-1'): """Return the given native string as a unicode string with the given encoding. """ assert_native(n) # In Python 3, the native string type is unicode return n def tonative(n, encoding='ISO-8859-1'): """Return the given string as a native string in the given encoding.""" # In Python 3, the native string type is unicode if isinstance(n, bytes): return n.decode(encoding) return n def assert_native(n): if not isinstance(n, str): raise TypeError('n must be a native str (got %s)' % type(n).__name__) # Some platforms don't expose HTTPSConnection, so handle it separately HTTPSConnection = getattr(http.client, 'HTTPSConnection', None) text_or_bytes = str, bytes ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpconfig.py0000644252176402575230000002265714307416152020415 0ustar00jaracoprimarygroup""" Configuration system for CherryPy. Configuration in CherryPy is implemented via dictionaries. Keys are strings which name the mapped value, which may be of any type. Architecture ------------ CherryPy Requests are part of an Application, which runs in a global context, and configuration data may apply to any of those three scopes: Global Configuration entries which apply everywhere are stored in cherrypy.config. Application Entries which apply to each mounted application are stored on the Application object itself, as 'app.config'. This is a two-level dict where each key is a path, or "relative URL" (for example, "/" or "/path/to/my/page"), and each value is a config dict. Usually, this data is provided in the call to tree.mount(root(), config=conf), although you may also use app.merge(conf). Request Each Request object possesses a single 'Request.config' dict. Early in the request process, this dict is populated by merging global config entries, Application entries (whose path equals or is a parent of Request.path_info), and any config acquired while looking up the page handler (see next). Declaration ----------- Configuration data may be supplied as a Python dictionary, as a filename, or as an open file object. When you supply a filename or file, CherryPy uses Python's builtin ConfigParser; you declare Application config by writing each path as a section header:: [/path/to/my/page] request.stream = True To declare global configuration entries, place them in a [global] section. You may also declare config entries directly on the classes and methods (page handlers) that make up your CherryPy application via the ``_cp_config`` attribute, set with the ``cherrypy.config`` decorator. For example:: @cherrypy.config(**{'tools.gzip.on': True}) class Demo: @cherrypy.expose @cherrypy.config(**{'request.show_tracebacks': False}) def index(self): return "Hello world" .. note:: This behavior is only guaranteed for the default dispatcher. Other dispatchers may have different restrictions on where you can attach config attributes. Namespaces ---------- Configuration keys are separated into namespaces by the first "." in the key. Current namespaces: engine Controls the 'application engine', including autoreload. These can only be declared in the global config. tree Grafts cherrypy.Application objects onto cherrypy.tree. These can only be declared in the global config. hooks Declares additional request-processing functions. log Configures the logging for each application. These can only be declared in the global or / config. request Adds attributes to each Request. response Adds attributes to each Response. server Controls the default HTTP server via cherrypy.server. These can only be declared in the global config. tools Runs and configures additional request-processing packages. wsgi Adds WSGI middleware to an Application's "pipeline". These can only be declared in the app's root config ("/"). checker Controls the 'checker', which looks for common errors in app state (including config) when the engine starts. Global config only. The only key that does not exist in a namespace is the "environment" entry. This special entry 'imports' other config entries from a template stored in cherrypy._cpconfig.environments[environment]. It only applies to the global config, and only when you use cherrypy.config.update. You can define your own namespaces to be called at the Global, Application, or Request level, by adding a named handler to cherrypy.config.namespaces, app.namespaces, or app.request_class.namespaces. The name can be any string, and the handler must be either a callable or a (Python 2.5 style) context manager. """ import cherrypy from cherrypy._cpcompat import text_or_bytes from cherrypy.lib import reprconf def _if_filename_register_autoreload(ob): """Register for autoreload if ob is a string (presumed filename).""" is_filename = isinstance(ob, text_or_bytes) is_filename and cherrypy.engine.autoreload.files.add(ob) def merge(base, other): """Merge one app config (from a dict, file, or filename) into another. If the given config is a filename, it will be appended to the list of files to monitor for "autoreload" changes. """ _if_filename_register_autoreload(other) # Load other into base for section, value_map in reprconf.Parser.load(other).items(): if not isinstance(value_map, dict): raise ValueError( 'Application config must include section headers, but the ' "config you tried to merge doesn't have any sections. " 'Wrap your config in another dict with paths as section ' "headers, for example: {'/': config}.") base.setdefault(section, {}).update(value_map) class Config(reprconf.Config): """The 'global' configuration data for the entire CherryPy process.""" def update(self, config): """Update self from a dict, file or filename.""" _if_filename_register_autoreload(config) super(Config, self).update(config) def _apply(self, config): """Update self from a dict.""" if isinstance(config.get('global'), dict): if len(config) > 1: cherrypy.checker.global_config_contained_paths = True config = config['global'] if 'tools.staticdir.dir' in config: config['tools.staticdir.section'] = 'global' super(Config, self)._apply(config) @staticmethod def __call__(**kwargs): """Decorate for page handlers to set _cp_config.""" def tool_decorator(f): _Vars(f).setdefault('_cp_config', {}).update(kwargs) return f return tool_decorator class _Vars(object): """Adapter allowing setting a default attribute on a function or class.""" def __init__(self, target): self.target = target def setdefault(self, key, default): if not hasattr(self.target, key): setattr(self.target, key, default) return getattr(self.target, key) # Sphinx begin config.environments Config.environments = environments = { 'staging': { 'engine.autoreload.on': False, 'checker.on': False, 'tools.log_headers.on': False, 'request.show_tracebacks': False, 'request.show_mismatched_params': False, }, 'production': { 'engine.autoreload.on': False, 'checker.on': False, 'tools.log_headers.on': False, 'request.show_tracebacks': False, 'request.show_mismatched_params': False, 'log.screen': False, }, 'embedded': { # For use with CherryPy embedded in another deployment stack. 'engine.autoreload.on': False, 'checker.on': False, 'tools.log_headers.on': False, 'request.show_tracebacks': False, 'request.show_mismatched_params': False, 'log.screen': False, 'engine.SIGHUP': None, 'engine.SIGTERM': None, }, 'test_suite': { 'engine.autoreload.on': False, 'checker.on': False, 'tools.log_headers.on': False, 'request.show_tracebacks': True, 'request.show_mismatched_params': True, 'log.screen': False, }, } # Sphinx end config.environments def _server_namespace_handler(k, v): """Config handler for the "server" namespace.""" atoms = k.split('.', 1) if len(atoms) > 1: # Special-case config keys of the form 'server.servername.socket_port' # to configure additional HTTP servers. if not hasattr(cherrypy, 'servers'): cherrypy.servers = {} servername, k = atoms if servername not in cherrypy.servers: from cherrypy import _cpserver cherrypy.servers[servername] = _cpserver.Server() # On by default, but 'on = False' can unsubscribe it (see below). cherrypy.servers[servername].subscribe() if k == 'on': if v: cherrypy.servers[servername].subscribe() else: cherrypy.servers[servername].unsubscribe() else: setattr(cherrypy.servers[servername], k, v) else: setattr(cherrypy.server, k, v) Config.namespaces['server'] = _server_namespace_handler def _engine_namespace_handler(k, v): """Config handler for the "engine" namespace.""" engine = cherrypy.engine if k in {'SIGHUP', 'SIGTERM'}: engine.subscribe(k, v) return if '.' in k: plugin, attrname = k.split('.', 1) plugin = getattr(engine, plugin) op = 'subscribe' if v else 'unsubscribe' sub_unsub = getattr(plugin, op, None) if attrname == 'on' and callable(sub_unsub): sub_unsub() return setattr(plugin, attrname, v) else: setattr(engine, k, v) Config.namespaces['engine'] = _engine_namespace_handler def _tree_namespace_handler(k, v): """Namespace handler for the 'tree' config namespace.""" if isinstance(v, dict): for script_name, app in v.items(): cherrypy.tree.graft(app, script_name) msg = 'Mounted: %s on %s' % (app, script_name or '/') cherrypy.engine.log(msg) else: cherrypy.tree.graft(v, v.script_name) cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/')) Config.namespaces['tree'] = _tree_namespace_handler ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpdispatch.py0000644252176402575230000006120014307416152020732 0ustar00jaracoprimarygroup"""CherryPy dispatchers. A 'dispatcher' is the object which looks up the 'page handler' callable and collects config for the current request based on the path_info, other request attributes, and the application architecture. The core calls the dispatcher as early as possible, passing it a 'path_info' argument. The default dispatcher discovers the page handler by matching path_info to a hierarchical arrangement of objects, starting at request.app.root. """ import string import sys import types try: classtype = (type, types.ClassType) except AttributeError: classtype = type import cherrypy class PageHandler(object): """Callable which sets response.body.""" def __init__(self, callable, *args, **kwargs): self.callable = callable self.args = args self.kwargs = kwargs @property def args(self): """The ordered args should be accessible from post dispatch hooks.""" return cherrypy.serving.request.args @args.setter def args(self, args): cherrypy.serving.request.args = args return cherrypy.serving.request.args @property def kwargs(self): """The named kwargs should be accessible from post dispatch hooks.""" return cherrypy.serving.request.kwargs @kwargs.setter def kwargs(self, kwargs): cherrypy.serving.request.kwargs = kwargs return cherrypy.serving.request.kwargs def __call__(self): try: return self.callable(*self.args, **self.kwargs) except TypeError: x = sys.exc_info()[1] try: test_callable_spec(self.callable, self.args, self.kwargs) except cherrypy.HTTPError: raise sys.exc_info()[1] except Exception: raise x raise def test_callable_spec(callable, callable_args, callable_kwargs): """ Inspect callable and test to see if the given args are suitable for it. When an error occurs during the handler's invoking stage there are 2 erroneous cases: 1. Too many parameters passed to a function which doesn't define one of *args or **kwargs. 2. Too little parameters are passed to the function. There are 3 sources of parameters to a cherrypy handler. 1. query string parameters are passed as keyword parameters to the handler. 2. body parameters are also passed as keyword parameters. 3. when partial matching occurs, the final path atoms are passed as positional args. Both the query string and path atoms are part of the URI. If they are incorrect, then a 404 Not Found should be raised. Conversely the body parameters are part of the request; if they are invalid a 400 Bad Request. """ show_mismatched_params = getattr( cherrypy.serving.request, 'show_mismatched_params', False) try: (args, varargs, varkw, defaults) = getargspec(callable) except TypeError: if isinstance(callable, object) and hasattr(callable, '__call__'): (args, varargs, varkw, defaults) = getargspec(callable.__call__) else: # If it wasn't one of our own types, re-raise # the original error raise if args and ( # For callable objects, which have a __call__(self) method hasattr(callable, '__call__') or # For normal methods inspect.ismethod(callable) ): # Strip 'self' args = args[1:] arg_usage = dict([(arg, 0,) for arg in args]) vararg_usage = 0 varkw_usage = 0 extra_kwargs = set() for i, value in enumerate(callable_args): try: arg_usage[args[i]] += 1 except IndexError: vararg_usage += 1 for key in callable_kwargs.keys(): try: arg_usage[key] += 1 except KeyError: varkw_usage += 1 extra_kwargs.add(key) # figure out which args have defaults. args_with_defaults = args[-len(defaults or []):] for i, val in enumerate(defaults or []): # Defaults take effect only when the arg hasn't been used yet. if arg_usage[args_with_defaults[i]] == 0: arg_usage[args_with_defaults[i]] += 1 missing_args = [] multiple_args = [] for key, usage in arg_usage.items(): if usage == 0: missing_args.append(key) elif usage > 1: multiple_args.append(key) if missing_args: # In the case where the method allows body arguments # there are 3 potential errors: # 1. not enough query string parameters -> 404 # 2. not enough body parameters -> 400 # 3. not enough path parts (partial matches) -> 404 # # We can't actually tell which case it is, # so I'm raising a 404 because that covers 2/3 of the # possibilities # # In the case where the method does not allow body # arguments it's definitely a 404. message = None if show_mismatched_params: message = 'Missing parameters: %s' % ','.join(missing_args) raise cherrypy.HTTPError(404, message=message) # the extra positional arguments come from the path - 404 Not Found if not varargs and vararg_usage > 0: raise cherrypy.HTTPError(404) body_params = cherrypy.serving.request.body.params or {} body_params = set(body_params.keys()) qs_params = set(callable_kwargs.keys()) - body_params if multiple_args: if qs_params.intersection(set(multiple_args)): # If any of the multiple parameters came from the query string then # it's a 404 Not Found error = 404 else: # Otherwise it's a 400 Bad Request error = 400 message = None if show_mismatched_params: message = 'Multiple values for parameters: '\ '%s' % ','.join(multiple_args) raise cherrypy.HTTPError(error, message=message) if not varkw and varkw_usage > 0: # If there were extra query string parameters, it's a 404 Not Found extra_qs_params = set(qs_params).intersection(extra_kwargs) if extra_qs_params: message = None if show_mismatched_params: message = 'Unexpected query string '\ 'parameters: %s' % ', '.join(extra_qs_params) raise cherrypy.HTTPError(404, message=message) # If there were any extra body parameters, it's a 400 Not Found extra_body_params = set(body_params).intersection(extra_kwargs) if extra_body_params: message = None if show_mismatched_params: message = 'Unexpected body parameters: '\ '%s' % ', '.join(extra_body_params) raise cherrypy.HTTPError(400, message=message) try: import inspect except ImportError: def test_callable_spec(callable, args, kwargs): # noqa: F811 return None else: def getargspec(callable): return inspect.getfullargspec(callable)[:4] class LateParamPageHandler(PageHandler): """When passing cherrypy.request.params to the page handler, we do not want to capture that dict too early; we want to give tools like the decoding tool a chance to modify the params dict in-between the lookup of the handler and the actual calling of the handler. This subclass takes that into account, and allows request.params to be 'bound late' (it's more complicated than that, but that's the effect). """ @property def kwargs(self): """Page handler kwargs (with cherrypy.request.params copied in).""" kwargs = cherrypy.serving.request.params.copy() if self._kwargs: kwargs.update(self._kwargs) return kwargs @kwargs.setter def kwargs(self, kwargs): cherrypy.serving.request.kwargs = kwargs self._kwargs = kwargs if sys.version_info < (3, 0): punctuation_to_underscores = string.maketrans( string.punctuation, '_' * len(string.punctuation)) def validate_translator(t): if not isinstance(t, str) or len(t) != 256: raise ValueError( 'The translate argument must be a str of len 256.') else: punctuation_to_underscores = str.maketrans( string.punctuation, '_' * len(string.punctuation)) def validate_translator(t): if not isinstance(t, dict): raise ValueError('The translate argument must be a dict.') class Dispatcher(object): """CherryPy Dispatcher which walks a tree of objects to find a handler. The tree is rooted at cherrypy.request.app.root, and each hierarchical component in the path_info argument is matched to a corresponding nested attribute of the root object. Matching handlers must have an 'exposed' attribute which evaluates to True. The special method name "index" matches a URI which ends in a slash ("/"). The special method name "default" may match a portion of the path_info (but only when no longer substring of the path_info matches some other object). This is the default, built-in dispatcher for CherryPy. """ dispatch_method_name = '_cp_dispatch' """ The name of the dispatch method that nodes may optionally implement to provide their own dynamic dispatch algorithm. """ def __init__(self, dispatch_method_name=None, translate=punctuation_to_underscores): validate_translator(translate) self.translate = translate if dispatch_method_name: self.dispatch_method_name = dispatch_method_name def __call__(self, path_info): """Set handler and config for the current request.""" request = cherrypy.serving.request func, vpath = self.find_handler(path_info) if func: # Decode any leftover %2F in the virtual_path atoms. vpath = [x.replace('%2F', '/') for x in vpath] request.handler = LateParamPageHandler(func, *vpath) else: request.handler = cherrypy.NotFound() def find_handler(self, path): """Return the appropriate page handler, plus any virtual path. This will return two objects. The first will be a callable, which can be used to generate page output. Any parameters from the query string or request body will be sent to that callable as keyword arguments. The callable is found by traversing the application's tree, starting from cherrypy.request.app.root, and matching path components to successive objects in the tree. For example, the URL "/path/to/handler" might return root.path.to.handler. The second object returned will be a list of names which are 'virtual path' components: parts of the URL which are dynamic, and were not used when looking up the handler. These virtual path components are passed to the handler as positional arguments. """ request = cherrypy.serving.request app = request.app root = app.root dispatch_name = self.dispatch_method_name # Get config for the root object/path. fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] fullpath_len = len(fullpath) segleft = fullpath_len nodeconf = {} if hasattr(root, '_cp_config'): nodeconf.update(root._cp_config) if '/' in app.config: nodeconf.update(app.config['/']) object_trail = [['root', root, nodeconf, segleft]] node = root iternames = fullpath[:] while iternames: name = iternames[0] # map to legal Python identifiers (e.g. replace '.' with '_') objname = name.translate(self.translate) nodeconf = {} subnode = getattr(node, objname, None) pre_len = len(iternames) if subnode is None: dispatch = getattr(node, dispatch_name, None) if dispatch and hasattr(dispatch, '__call__') and not \ getattr(dispatch, 'exposed', False) and \ pre_len > 1: # Don't expose the hidden 'index' token to _cp_dispatch # We skip this if pre_len == 1 since it makes no sense # to call a dispatcher when we have no tokens left. index_name = iternames.pop() subnode = dispatch(vpath=iternames) iternames.append(index_name) else: # We didn't find a path, but keep processing in case there # is a default() handler. iternames.pop(0) else: # We found the path, remove the vpath entry iternames.pop(0) segleft = len(iternames) if segleft > pre_len: # No path segment was removed. Raise an error. raise cherrypy.CherryPyException( 'A vpath segment was added. Custom dispatchers may only ' 'remove elements. While trying to process ' '{0} in {1}'.format(name, fullpath) ) elif segleft == pre_len: # Assume that the handler used the current path segment, but # did not pop it. This allows things like # return getattr(self, vpath[0], None) iternames.pop(0) segleft -= 1 node = subnode if node is not None: # Get _cp_config attached to this node. if hasattr(node, '_cp_config'): nodeconf.update(node._cp_config) # Mix in values from app.config for this path. existing_len = fullpath_len - pre_len if existing_len != 0: curpath = '/' + '/'.join(fullpath[0:existing_len]) else: curpath = '' new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] for seg in new_segs: curpath += '/' + seg if curpath in app.config: nodeconf.update(app.config[curpath]) object_trail.append([name, node, nodeconf, segleft]) def set_conf(): """Collapse all object_trail config into cherrypy.request.config. """ base = cherrypy.config.copy() # Note that we merge the config from each node # even if that node was None. for name, obj, conf, segleft in object_trail: base.update(conf) if 'tools.staticdir.dir' in conf: base['tools.staticdir.section'] = '/' + \ '/'.join(fullpath[0:fullpath_len - segleft]) return base # Try successive objects (reverse order) num_candidates = len(object_trail) - 1 for i in range(num_candidates, -1, -1): name, candidate, nodeconf, segleft = object_trail[i] if candidate is None: continue # Try a "default" method on the current leaf. if hasattr(candidate, 'default'): defhandler = candidate.default if getattr(defhandler, 'exposed', False): # Insert any extra _cp_config from the default handler. conf = getattr(defhandler, '_cp_config', {}) object_trail.insert( i + 1, ['default', defhandler, conf, segleft]) request.config = set_conf() # See https://github.com/cherrypy/cherrypy/issues/613 request.is_index = path.endswith('/') return defhandler, fullpath[fullpath_len - segleft:-1] # Uncomment the next line to restrict positional params to # "default". # if i < num_candidates - 2: continue # Try the current leaf. if getattr(candidate, 'exposed', False): request.config = set_conf() if i == num_candidates: # We found the extra ".index". Mark request so tools # can redirect if path_info has no trailing slash. request.is_index = True else: # We're not at an 'index' handler. Mark request so tools # can redirect if path_info has NO trailing slash. # Note that this also includes handlers which take # positional parameters (virtual paths). request.is_index = False return candidate, fullpath[fullpath_len - segleft:-1] # We didn't find anything request.config = set_conf() return None, [] class MethodDispatcher(Dispatcher): """Additional dispatch based on cherrypy.request.method.upper(). Methods named GET, POST, etc will be called on an exposed class. The method names must be all caps; the appropriate Allow header will be output showing all capitalized method names as allowable HTTP verbs. Note that the containing class must be exposed, not the methods. """ def __call__(self, path_info): """Set handler and config for the current request.""" request = cherrypy.serving.request resource, vpath = self.find_handler(path_info) if resource: # Set Allow header avail = [m for m in dir(resource) if m.isupper()] if 'GET' in avail and 'HEAD' not in avail: avail.append('HEAD') avail.sort() cherrypy.serving.response.headers['Allow'] = ', '.join(avail) # Find the subhandler meth = request.method.upper() func = getattr(resource, meth, None) if func is None and meth == 'HEAD': func = getattr(resource, 'GET', None) if func: # Grab any _cp_config on the subhandler. if hasattr(func, '_cp_config'): request.config.update(func._cp_config) # Decode any leftover %2F in the virtual_path atoms. vpath = [x.replace('%2F', '/') for x in vpath] request.handler = LateParamPageHandler(func, *vpath) else: request.handler = cherrypy.HTTPError(405) else: request.handler = cherrypy.NotFound() class RoutesDispatcher(object): """A Routes based dispatcher for CherryPy.""" def __init__(self, full_result=False, **mapper_options): """ Routes dispatcher Set full_result to True if you wish the controller and the action to be passed on to the page handler parameters. By default they won't be. """ import routes self.full_result = full_result self.controllers = {} self.mapper = routes.Mapper(**mapper_options) self.mapper.controller_scan = self.controllers.keys def connect(self, name, route, controller, **kwargs): self.controllers[name] = controller self.mapper.connect(name, route, controller=name, **kwargs) def redirect(self, url): raise cherrypy.HTTPRedirect(url) def __call__(self, path_info): """Set handler and config for the current request.""" func = self.find_handler(path_info) if func: cherrypy.serving.request.handler = LateParamPageHandler(func) else: cherrypy.serving.request.handler = cherrypy.NotFound() def find_handler(self, path_info): """Find the right page handler, and set request.config.""" import routes request = cherrypy.serving.request config = routes.request_config() config.mapper = self.mapper if hasattr(request, 'wsgi_environ'): config.environ = request.wsgi_environ config.host = request.headers.get('Host', None) config.protocol = request.scheme config.redirect = self.redirect result = self.mapper.match(path_info) config.mapper_dict = result params = {} if result: params = result.copy() if not self.full_result: params.pop('controller', None) params.pop('action', None) request.params.update(params) # Get config for the root object/path. request.config = base = cherrypy.config.copy() curpath = '' def merge(nodeconf): if 'tools.staticdir.dir' in nodeconf: nodeconf['tools.staticdir.section'] = curpath or '/' base.update(nodeconf) app = request.app root = app.root if hasattr(root, '_cp_config'): merge(root._cp_config) if '/' in app.config: merge(app.config['/']) # Mix in values from app.config. atoms = [x for x in path_info.split('/') if x] if atoms: last = atoms.pop() else: last = None for atom in atoms: curpath = '/'.join((curpath, atom)) if curpath in app.config: merge(app.config[curpath]) handler = None if result: controller = result.get('controller') controller = self.controllers.get(controller, controller) if controller: if isinstance(controller, classtype): controller = controller() # Get config from the controller. if hasattr(controller, '_cp_config'): merge(controller._cp_config) action = result.get('action') if action is not None: handler = getattr(controller, action, None) # Get config from the handler if hasattr(handler, '_cp_config'): merge(handler._cp_config) else: handler = controller # Do the last path atom here so it can # override the controller's _cp_config. if last: curpath = '/'.join((curpath, last)) if curpath in app.config: merge(app.config[curpath]) return handler def XMLRPCDispatcher(next_dispatcher=Dispatcher()): from cherrypy.lib import xmlrpcutil def xmlrpc_dispatch(path_info): path_info = xmlrpcutil.patched_path(path_info) return next_dispatcher(path_info) return xmlrpc_dispatch def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains): """ Select a different handler based on the Host header. This can be useful when running multiple sites within one CP server. It allows several domains to point to different parts of a single website structure. For example:: http://www.domain.example -> root http://www.domain2.example -> root/domain2/ http://www.domain2.example:443 -> root/secure can be accomplished via the following config:: [/] request.dispatch = cherrypy.dispatch.VirtualHost( **{'www.domain2.example': '/domain2', 'www.domain2.example:443': '/secure', }) next_dispatcher The next dispatcher object in the dispatch chain. The VirtualHost dispatcher adds a prefix to the URL and calls another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). use_x_forwarded_host If True (the default), any "X-Forwarded-Host" request header will be used instead of the "Host" header. This is commonly added by HTTP servers (such as Apache) when proxying. ``**domains`` A dict of {host header value: virtual prefix} pairs. The incoming "Host" request header is looked up in this dict, and, if a match is found, the corresponding "virtual prefix" value will be prepended to the URL path before calling the next dispatcher. Note that you often need separate entries for "example.com" and "www.example.com". In addition, "Host" headers may contain the port number. """ from cherrypy.lib import httputil def vhost_dispatch(path_info): request = cherrypy.serving.request header = request.headers.get domain = header('Host', '') if use_x_forwarded_host: domain = header('X-Forwarded-Host', domain) prefix = domains.get(domain, '') if prefix: path_info = httputil.urljoin(prefix, path_info) result = next_dispatcher(path_info) # Touch up staticdir config. See # https://github.com/cherrypy/cherrypy/issues/614. section = request.config.get('tools.staticdir.section') if section: section = section[len(prefix):] request.config['tools.staticdir.section'] = section return result return vhost_dispatch ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cperror.py0000644252176402575230000005507714307416152020303 0ustar00jaracoprimarygroup"""Exception classes for CherryPy. CherryPy provides (and uses) exceptions for declaring that the HTTP response should be a status other than the default "200 OK". You can ``raise`` them like normal Python exceptions. You can also call them and they will raise themselves; this means you can set an :class:`HTTPError` or :class:`HTTPRedirect` as the :attr:`request.handler`. .. _redirectingpost: Redirecting POST ================ When you GET a resource and are redirected by the server to another Location, there's generally no problem since GET is both a "safe method" (there should be no side-effects) and an "idempotent method" (multiple calls are no different than a single call). POST, however, is neither safe nor idempotent--if you charge a credit card, you don't want to be charged twice by a redirect! For this reason, *none* of the 3xx responses permit a user-agent (browser) to resubmit a POST on redirection without first confirming the action with the user: ===== ================================= =========== 300 Multiple Choices Confirm with the user 301 Moved Permanently Confirm with the user 302 Found (Object moved temporarily) Confirm with the user 303 See Other GET the new URI; no confirmation 304 Not modified for conditional GET only; POST should not raise this error 305 Use Proxy Confirm with the user 307 Temporary Redirect Confirm with the user 308 Permanent Redirect No confirmation ===== ================================= =========== However, browsers have historically implemented these restrictions poorly; in particular, many browsers do not force the user to confirm 301, 302 or 307 when redirecting POST. For this reason, CherryPy defaults to 303, which most user-agents appear to have implemented correctly. Therefore, if you raise HTTPRedirect for a POST request, the user-agent will most likely attempt to GET the new URI (without asking for confirmation from the user). We realize this is confusing for developers, but it's the safest thing we could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` or any other 3xx status if you know what you're doing, but given the environment, we couldn't let any of those be the default. Custom Error Handling ===================== .. image:: /refman/cperrors.gif Anticipated HTTP responses -------------------------- The 'error_page' config namespace can be used to provide custom HTML output for expected responses (like 404 Not Found). Supply a filename from which the output will be read. The contents will be interpolated with the values %(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python `string formatting `_. :: _cp_config = { 'error_page.404': os.path.join(localDir, "static/index.html") } Beginning in version 3.1, you may also provide a function or other callable as an error_page entry. It will be passed the same status, message, traceback and version arguments that are interpolated into templates:: def error_page_402(status, message, traceback, version): return "Error %s - Well, I'm very sorry but you haven't paid!" % status cherrypy.config.update({'error_page.402': error_page_402}) Also in 3.1, in addition to the numbered error codes, you may also supply "error_page.default" to handle all codes which do not have their own error_page entry. Unanticipated errors -------------------- CherryPy also has a generic error handling mechanism: whenever an unanticipated error occurs in your code, it will call :func:`Request.error_response` to set the response status, headers, and body. By default, this is the same output as :class:`HTTPError(500) `. If you want to provide some other behavior, you generally replace "request.error_response". Here is some sample code that shows how to display a custom error message and send an e-mail containing the error:: from cherrypy import _cperror def handle_error(): cherrypy.response.status = 500 cherrypy.response.body = [ "Sorry, an error occurred" ] sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc()) @cherrypy.config(**{'request.error_response': handle_error}) class Root: pass Note that you have to explicitly set :attr:`response.body ` and not simply return an error message as a result. """ import io import contextlib import urllib.parse from sys import exc_info as _exc_info from traceback import format_exception as _format_exception from xml.sax import saxutils import html from more_itertools import always_iterable import cherrypy from cherrypy._cpcompat import ntob from cherrypy._cpcompat import tonative from cherrypy._helper import classproperty from cherrypy.lib import httputil as _httputil class CherryPyException(Exception): """A base class for CherryPy exceptions.""" pass class InternalRedirect(CherryPyException): """Exception raised to switch to the handler for a different URL. This exception will redirect processing to another path within the site (without informing the client). Provide the new path as an argument when raising the exception. Provide any params in the querystring for the new URL. """ def __init__(self, path, query_string=''): self.request = cherrypy.serving.request self.query_string = query_string if '?' in path: # Separate any params included in the path path, self.query_string = path.split('?', 1) # Note that urljoin will "do the right thing" whether url is: # 1. a URL relative to root (e.g. "/dummy") # 2. a URL relative to the current path # Note that any query string will be discarded. path = urllib.parse.urljoin(self.request.path_info, path) # Set a 'path' member attribute so that code which traps this # error can have access to it. self.path = path CherryPyException.__init__(self, path, self.query_string) class HTTPRedirect(CherryPyException): """Exception raised when the request should be redirected. This exception will force a HTTP redirect to the URL or URL's you give it. The new URL must be passed as the first argument to the Exception, e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. If a URL is absolute, it will be used as-is. If it is relative, it is assumed to be relative to the current cherrypy.request.path_info. If one of the provided URL is a unicode object, it will be encoded using the default encoding or the one passed in parameter. There are multiple types of redirect, from which you can select via the ``status`` argument. If you do not provide a ``status`` arg, it defaults to 303 (or 302 if responding with HTTP/1.0). Examples:: raise cherrypy.HTTPRedirect("") raise cherrypy.HTTPRedirect("/abs/path", 307) raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) See :ref:`redirectingpost` for additional caveats. """ urls = None """The list of URL's to emit.""" encoding = 'utf-8' """The encoding when passed urls are not native strings""" def __init__(self, urls, status=None, encoding=None): self.urls = abs_urls = [ # Note that urljoin will "do the right thing" whether url is: # 1. a complete URL with host (e.g. "http://www.example.com/test") # 2. a URL relative to root (e.g. "/dummy") # 3. a URL relative to the current path # Note that any query string in cherrypy.request is discarded. urllib.parse.urljoin( cherrypy.url(), tonative(url, encoding or self.encoding), ) for url in always_iterable(urls) ] status = ( int(status) if status is not None else self.default_status ) if not 300 <= status <= 399: raise ValueError('status must be between 300 and 399.') CherryPyException.__init__(self, abs_urls, status) @classproperty def default_status(cls): """ The default redirect status for the request. RFC 2616 indicates a 301 response code fits our goal; however, browser support for 301 is quite messy. Use 302/303 instead. See http://www.alanflavell.org.uk/www/post-redirect.html """ return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302 @property def status(self): """The integer HTTP status code to emit.""" _, status = self.args[:2] return status def set_response(self): """Modify cherrypy.response status, headers, and body to represent self. CherryPy uses this internally, but you can also use it to create an HTTPRedirect object and set its output without *raising* the exception. """ response = cherrypy.serving.response response.status = status = self.status if status in (300, 301, 302, 303, 307, 308): response.headers['Content-Type'] = 'text/html;charset=utf-8' # "The ... URI SHOULD be given by the Location field # in the response." response.headers['Location'] = self.urls[0] # "Unless the request method was HEAD, the entity of the response # SHOULD contain a short hypertext note with a hyperlink to the # new URI(s)." msg = { 300: 'This resource can be found at ', 301: 'This resource has permanently moved to ', 302: 'This resource resides temporarily at ', 303: 'This resource can be found at ', 307: 'This resource has moved temporarily to ', 308: 'This resource has been moved to ', }[status] msg += '%s.' msgs = [ msg % (saxutils.quoteattr(u), html.escape(u, quote=False)) for u in self.urls ] response.body = ntob('
\n'.join(msgs), 'utf-8') # Previous code may have set C-L, so we have to reset it # (allow finalize to set it). response.headers.pop('Content-Length', None) elif status == 304: # Not Modified. # "The response MUST include the following header fields: # Date, unless its omission is required by section 14.18.1" # The "Date" header should have been set in Response.__init__ # "...the response SHOULD NOT include other entity-headers." for key in ('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-Location', 'Content-MD5', 'Content-Range', 'Content-Type', 'Expires', 'Last-Modified'): if key in response.headers: del response.headers[key] # "The 304 response MUST NOT contain a message-body." response.body = None # Previous code may have set C-L, so we have to reset it. response.headers.pop('Content-Length', None) elif status == 305: # Use Proxy. # self.urls[0] should be the URI of the proxy. response.headers['Location'] = ntob(self.urls[0], 'utf-8') response.body = None # Previous code may have set C-L, so we have to reset it. response.headers.pop('Content-Length', None) else: raise ValueError('The %s status code is unknown.' % status) def __call__(self): """Use this exception as a request.handler (raise self).""" raise self def clean_headers(status): """Remove any headers which should not apply to an error response.""" response = cherrypy.serving.response # Remove headers which applied to the original content, # but do not apply to the error page. respheaders = response.headers for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After', 'Vary', 'Content-Encoding', 'Content-Length', 'Expires', 'Content-Location', 'Content-MD5', 'Last-Modified']: if key in respheaders: del respheaders[key] if status != 416: # A server sending a response with status code 416 (Requested # range not satisfiable) SHOULD include a Content-Range field # with a byte-range-resp-spec of "*". The instance-length # specifies the current length of the selected resource. # A response with status code 206 (Partial Content) MUST NOT # include a Content-Range field with a byte-range- resp-spec of "*". if 'Content-Range' in respheaders: del respheaders['Content-Range'] class HTTPError(CherryPyException): """Exception used to return an HTTP error code (4xx-5xx) to the client. This exception can be used to automatically send a response using a http status code, with an appropriate error page. It takes an optional ``status`` argument (which must be between 400 and 599); it defaults to 500 ("Internal Server Error"). It also takes an optional ``message`` argument, which will be returned in the response body. See `RFC2616 `_ for a complete list of available error codes and when to use them. Examples:: raise cherrypy.HTTPError(403) raise cherrypy.HTTPError( "403 Forbidden", "You are not allowed to access this resource.") """ status = None """The HTTP status code. May be of type int or str (with a Reason-Phrase). """ code = None """The integer HTTP status code.""" reason = None """The HTTP Reason-Phrase string.""" def __init__(self, status=500, message=None): self.status = status try: self.code, self.reason, defaultmsg = _httputil.valid_status(status) except ValueError: raise self.__class__(500, _exc_info()[1].args[0]) if self.code < 400 or self.code > 599: raise ValueError('status must be between 400 and 599.') # See http://www.python.org/dev/peps/pep-0352/ # self.message = message self._message = message or defaultmsg CherryPyException.__init__(self, status, message) def set_response(self): """Modify cherrypy.response status, headers, and body to represent self. CherryPy uses this internally, but you can also use it to create an HTTPError object and set its output without *raising* the exception. """ response = cherrypy.serving.response clean_headers(self.code) # In all cases, finalize will be called after this method, # so don't bother cleaning up response values here. response.status = self.status tb = None if cherrypy.serving.request.show_tracebacks: tb = format_exc() response.headers.pop('Content-Length', None) content = self.get_error_page(self.status, traceback=tb, message=self._message) response.body = content _be_ie_unfriendly(self.code) def get_error_page(self, *args, **kwargs): return get_error_page(*args, **kwargs) def __call__(self): """Use this exception as a request.handler (raise self).""" raise self @classmethod @contextlib.contextmanager def handle(cls, exception, status=500, message=''): """Translate exception into an HTTPError.""" try: yield except exception as exc: raise cls(status, message or str(exc)) class NotFound(HTTPError): """Exception raised when a URL could not be mapped to any handler (404). This is equivalent to raising :class:`HTTPError("404 Not Found") `. """ def __init__(self, path=None): if path is None: request = cherrypy.serving.request path = request.script_name + request.path_info self.args = (path,) HTTPError.__init__(self, 404, "The path '%s' was not found." % path) _HTTPErrorTemplate = ''' %(status)s

%(status)s

%(message)s

%(traceback)s
''' def get_error_page(status, **kwargs): """Return an HTML page, containing a pretty error response. status should be an int or a str. kwargs will be interpolated into the page template. """ try: code, reason, message = _httputil.valid_status(status) except ValueError: raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) # We can't use setdefault here, because some # callers send None for kwarg values. if kwargs.get('status') is None: kwargs['status'] = '%s %s' % (code, reason) if kwargs.get('message') is None: kwargs['message'] = message if kwargs.get('traceback') is None: kwargs['traceback'] = '' if kwargs.get('version') is None: kwargs['version'] = cherrypy.__version__ for k, v in kwargs.items(): if v is None: kwargs[k] = '' else: kwargs[k] = html.escape(kwargs[k], quote=False) # Use a custom template or callable for the error page? pages = cherrypy.serving.request.error_page error_page = pages.get(code) or pages.get('default') # Default template, can be overridden below. template = _HTTPErrorTemplate if error_page: try: if hasattr(error_page, '__call__'): # The caller function may be setting headers manually, # so we delegate to it completely. We may be returning # an iterator as well as a string here. # # We *must* make sure any content is not unicode. result = error_page(**kwargs) if cherrypy.lib.is_iterator(result): from cherrypy.lib.encoding import UTF8StreamEncoder return UTF8StreamEncoder(result) elif isinstance(result, str): return result.encode('utf-8') else: if not isinstance(result, bytes): raise ValueError( 'error page function did not ' 'return a bytestring, str or an ' 'iterator - returned object of type %s.' % (type(result).__name__)) return result else: # Load the template from this path. with io.open(error_page, newline='') as f: template = f.read() except Exception: e = _format_exception(*_exc_info())[-1] m = kwargs['message'] if m: m += '
' m += 'In addition, the custom error page failed:\n
%s' % e kwargs['message'] = m response = cherrypy.serving.response response.headers['Content-Type'] = 'text/html;charset=utf-8' result = template % kwargs return result.encode('utf-8') _ie_friendly_error_sizes = { 400: 512, 403: 256, 404: 512, 405: 256, 406: 512, 408: 512, 409: 512, 410: 256, 500: 512, 501: 512, 505: 512, } def _be_ie_unfriendly(status): response = cherrypy.serving.response # For some statuses, Internet Explorer 5+ shows "friendly error # messages" instead of our response.body if the body is smaller # than a given size. Fix this by returning a body over that size # (by adding whitespace). # See http://support.microsoft.com/kb/q218155/ s = _ie_friendly_error_sizes.get(status, 0) if s: s += 1 # Since we are issuing an HTTP error status, we assume that # the entity is short, and we should just collapse it. content = response.collapse_body() content_length = len(content) if content_length and content_length < s: # IN ADDITION: the response must be written to IE # in one chunk or it will still get replaced! Bah. content = content + (b' ' * (s - content_length)) response.body = content response.headers['Content-Length'] = str(len(content)) def format_exc(exc=None): """Return exc (or sys.exc_info if None), formatted.""" try: if exc is None: exc = _exc_info() if exc == (None, None, None): return '' import traceback return ''.join(traceback.format_exception(*exc)) finally: del exc def bare_error(extrabody=None): """Produce status, headers, body for a critical error. Returns a triple without calling any other questionable functions, so it should be as error-free as possible. Call it from an HTTP server if you get errors outside of the request. If extrabody is None, a friendly but rather unhelpful error message is set in the body. If extrabody is a string, it will be appended as-is to the body. """ # The whole point of this function is to be a last line-of-defense # in handling errors. That is, it must not raise any errors itself; # it cannot be allowed to fail. Therefore, don't add to it! # In particular, don't call any other CP functions. body = b'Unrecoverable error in the server.' if extrabody is not None: if not isinstance(extrabody, bytes): extrabody = extrabody.encode('utf-8') body += b'\n' + extrabody return (b'500 Internal Server Error', [(b'Content-Type', b'text/plain'), (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))], [body]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/cherrypy/_cplogging.py0000644252176402575230000004006614424762003020567 0ustar00jaracoprimarygroup""" Simple config ============= Although CherryPy uses the :mod:`Python logging module `, it does so behind the scenes so that simple logging is simple, but complicated logging is still possible. "Simple" logging means that you can log to the screen (i.e. console/stdout) or to a file, and that you can easily have separate error and access log files. Here are the simplified logging settings. You use these by adding lines to your config file or dict. You should set these at either the global level or per application (see next), but generally not both. * ``log.screen``: Set this to True to have both "error" and "access" messages printed to stdout. * ``log.access_file``: Set this to an absolute filename where you want "access" messages written. * ``log.error_file``: Set this to an absolute filename where you want "error" messages written. Many events are automatically logged; to log your own application events, call :func:`cherrypy.log`. Architecture ============ Separate scopes --------------- CherryPy provides log managers at both the global and application layers. This means you can have one set of logging rules for your entire site, and another set of rules specific to each application. The global log manager is found at :func:`cherrypy.log`, and the log manager for each application is found at :attr:`app.log`. If you're inside a request, the latter is reachable from ``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain a reference to the ``app``: either the return value of :func:`tree.mount()` or, if you used :func:`quickstart()` instead, via ``cherrypy.tree.apps['/']``. By default, the global logs are named "cherrypy.error" and "cherrypy.access", and the application logs are named "cherrypy.error.2378745" and "cherrypy.access.2378745" (the number is the id of the Application object). This means that the application logs "bubble up" to the site logs, so if your application has no log handlers, the site-level handlers will still log the messages. Errors vs. Access ----------------- Each log manager handles both "access" messages (one per HTTP request) and "error" messages (everything else). Note that the "error" log is not just for errors! The format of access messages is highly formalized, but the error log isn't--it receives messages from a variety of sources (including full error tracebacks, if enabled). If you are logging the access log and error log to the same source, then there is a possibility that a specially crafted error message may replicate an access log message as described in CWE-117. In this case it is the application developer's responsibility to manually escape data before using CherryPy's log() functionality, or they may create an application that is vulnerable to CWE-117. This would be achieved by using a custom handler escape any special characters, and attached as described below. Custom Handlers =============== The simple settings above work by manipulating Python's standard :mod:`logging` module. So when you need something more complex, the full power of the standard module is yours to exploit. You can borrow or create custom handlers, formats, filters, and much more. Here's an example that skips the standard FileHandler and uses a RotatingFileHandler instead: :: #python log = app.log # Remove the default FileHandlers if present. log.error_file = "" log.access_file = "" maxBytes = getattr(log, "rot_maxBytes", 10000000) backupCount = getattr(log, "rot_backupCount", 1000) # Make a new RotatingFileHandler for the error log. fname = getattr(log, "rot_error_file", "error.log") h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) h.setLevel(DEBUG) h.setFormatter(_cplogging.logfmt) log.error_log.addHandler(h) # Make a new RotatingFileHandler for the access log. fname = getattr(log, "rot_access_file", "access.log") h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) h.setLevel(DEBUG) h.setFormatter(_cplogging.logfmt) log.access_log.addHandler(h) The ``rot_*`` attributes are pulled straight from the application log object. Since "log.*" config entries simply set attributes on the log object, you can add custom attributes to your heart's content. Note that these handlers are used ''instead'' of the default, simple handlers outlined above (so don't set the "log.error_file" config entry, for example). """ import datetime import logging import os import sys import cherrypy from cherrypy import _cperror # Silence the no-handlers "warning" (stderr write!) in stdlib logging logging.Logger.manager.emittedNoHandlerWarning = 1 logfmt = logging.Formatter('%(message)s') class NullHandler(logging.Handler): """A no-op logging handler to silence the logging.lastResort handler.""" def handle(self, record): pass def emit(self, record): pass def createLock(self): self.lock = None class LogManager(object): """An object to assist both simple and advanced logging. ``cherrypy.log`` is an instance of this class. """ appid = None """The id() of the Application object which owns this log manager. If this is a global log manager, appid is None.""" error_log = None """The actual :class:`logging.Logger` instance for error messages.""" access_log = None """The actual :class:`logging.Logger` instance for access messages.""" access_log_format = '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' logger_root = None """The "top-level" logger name. This string will be used as the first segment in the Logger names. The default is "cherrypy", for example, in which case the Logger names will be of the form:: cherrypy.error. cherrypy.access. """ def __init__(self, appid=None, logger_root='cherrypy'): self.logger_root = logger_root self.appid = appid if appid is None: self.error_log = logging.getLogger('%s.error' % logger_root) self.access_log = logging.getLogger('%s.access' % logger_root) else: self.error_log = logging.getLogger( '%s.error.%s' % (logger_root, appid)) self.access_log = logging.getLogger( '%s.access.%s' % (logger_root, appid)) self.error_log.setLevel(logging.INFO) self.access_log.setLevel(logging.INFO) # Silence the no-handlers "warning" (stderr write!) in stdlib logging self.error_log.addHandler(NullHandler()) self.access_log.addHandler(NullHandler()) cherrypy.engine.subscribe('graceful', self.reopen_files) def reopen_files(self): """Close and reopen all file handlers.""" for log in (self.error_log, self.access_log): for h in log.handlers: if isinstance(h, logging.FileHandler): h.acquire() h.stream.close() h.stream = open(h.baseFilename, h.mode) h.release() def error(self, msg='', context='', severity=logging.INFO, traceback=False): """Write the given ``msg`` to the error log. This is not just for errors! Applications may call this at any time to log application-specific information. If ``traceback`` is True, the traceback of the current exception (if any) will be appended to ``msg``. """ exc_info = None if traceback: exc_info = _cperror._exc_info() self.error_log.log( severity, ' '.join((self.time(), context, msg)), exc_info=exc_info, ) def __call__(self, *args, **kwargs): """An alias for ``error``.""" return self.error(*args, **kwargs) def access(self): """Write to the access log (in Apache/NCSA Combined Log format). See the `apache documentation `_ for format details. CherryPy calls this automatically for you. Note there are no arguments; it collects the data itself from :class:`cherrypy.request`. Like Apache started doing in 2.0.46, non-printable and other special characters in %r (and we expand that to all parts) are escaped using \\xhh sequences, where hh stands for the hexadecimal representation of the raw byte. Exceptions from this rule are " and \\, which are escaped by prepending a backslash, and all whitespace characters, which are written in their C-style notation (\\n, \\t, etc). """ request = cherrypy.serving.request remote = request.remote response = cherrypy.serving.response outheaders = response.headers inheaders = request.headers if response.output_status is None: status = '-' else: status = response.output_status.split(b' ', 1)[0] status = status.decode('ISO-8859-1') atoms = {'h': remote.name or remote.ip, 'l': '-', 'u': getattr(request, 'login', None) or '-', 't': self.time(), 'r': request.request_line, 's': status, 'b': dict.get(outheaders, 'Content-Length', '') or '-', 'f': dict.get(inheaders, 'Referer', ''), 'a': dict.get(inheaders, 'User-Agent', ''), 'o': dict.get(inheaders, 'Host', '-'), 'i': request.unique_id, 'z': LazyRfc3339UtcTime(), } for k, v in atoms.items(): if not isinstance(v, str): v = str(v) v = v.replace('"', '\\"').encode('utf8') # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc # and backslash for us. All we have to do is strip the quotes. v = repr(v)[2:-1] # in python 3.0 the repr of bytes (as returned by encode) # uses double \'s. But then the logger escapes them yet, again # resulting in quadruple slashes. Remove the extra one here. v = v.replace('\\\\', '\\') # Escape double-quote. atoms[k] = v try: self.access_log.log( logging.INFO, self.access_log_format.format(**atoms)) except Exception: self(traceback=True) def time(self): """Return now() in Apache Common Log Format (no timezone).""" now = datetime.datetime.now() monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] month = monthnames[now.month - 1].capitalize() return ('[%02d/%s/%04d:%02d:%02d:%02d]' % (now.day, month, now.year, now.hour, now.minute, now.second)) def _get_builtin_handler(self, log, key): for h in log.handlers: if getattr(h, '_cpbuiltin', None) == key: return h # ------------------------- Screen handlers ------------------------- # def _set_screen_handler(self, log, enable, stream=None): h = self._get_builtin_handler(log, 'screen') if enable: if not h: if stream is None: stream = sys.stderr h = logging.StreamHandler(stream) h.setFormatter(logfmt) h._cpbuiltin = 'screen' log.addHandler(h) elif h: log.handlers.remove(h) @property def screen(self): """Turn stderr/stdout logging on or off. If you set this to True, it'll add the appropriate StreamHandler for you. If you set it to False, it will remove the handler. """ h = self._get_builtin_handler has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen') return bool(has_h) @screen.setter def screen(self, newvalue): self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) # -------------------------- File handlers -------------------------- # def _add_builtin_file_handler(self, log, fname): h = logging.FileHandler(fname) h.setFormatter(logfmt) h._cpbuiltin = 'file' log.addHandler(h) def _set_file_handler(self, log, filename): h = self._get_builtin_handler(log, 'file') if filename: if h: if h.baseFilename != os.path.abspath(filename): h.close() log.handlers.remove(h) self._add_builtin_file_handler(log, filename) else: self._add_builtin_file_handler(log, filename) else: if h: h.close() log.handlers.remove(h) @property def error_file(self): """The filename for self.error_log. If you set this to a string, it'll add the appropriate FileHandler for you. If you set it to ``None`` or ``''``, it will remove the handler. """ h = self._get_builtin_handler(self.error_log, 'file') if h: return h.baseFilename return '' @error_file.setter def error_file(self, newvalue): self._set_file_handler(self.error_log, newvalue) @property def access_file(self): """The filename for self.access_log. If you set this to a string, it'll add the appropriate FileHandler for you. If you set it to ``None`` or ``''``, it will remove the handler. """ h = self._get_builtin_handler(self.access_log, 'file') if h: return h.baseFilename return '' @access_file.setter def access_file(self, newvalue): self._set_file_handler(self.access_log, newvalue) # ------------------------- WSGI handlers ------------------------- # def _set_wsgi_handler(self, log, enable): h = self._get_builtin_handler(log, 'wsgi') if enable: if not h: h = WSGIErrorHandler() h.setFormatter(logfmt) h._cpbuiltin = 'wsgi' log.addHandler(h) elif h: log.handlers.remove(h) @property def wsgi(self): """Write errors to wsgi.errors. If you set this to True, it'll add the appropriate :class:`WSGIErrorHandler` for you (which writes errors to ``wsgi.errors``). If you set it to False, it will remove the handler. """ return bool(self._get_builtin_handler(self.error_log, 'wsgi')) @wsgi.setter def wsgi(self, newvalue): self._set_wsgi_handler(self.error_log, newvalue) class WSGIErrorHandler(logging.Handler): "A handler class which writes logging records to environ['wsgi.errors']." def flush(self): """Flushes the stream.""" try: stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') except (AttributeError, KeyError): pass else: stream.flush() def emit(self, record): """Emit a record.""" try: stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') except (AttributeError, KeyError): pass else: try: msg = self.format(record) fs = '%s\n' import types # if no unicode support... if not hasattr(types, 'UnicodeType'): stream.write(fs % msg) else: try: stream.write(fs % msg) except UnicodeError: stream.write(fs % msg.encode('UTF-8')) self.flush() except Exception: self.handleError(record) class LazyRfc3339UtcTime(object): def __str__(self): """Return utcnow() in RFC3339 UTC Format.""" iso_formatted_now = datetime.datetime.utcnow().isoformat('T') return f'{iso_formatted_now!s}Z' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpmodpy.py0000644252176402575230000002553014307416152020271 0ustar00jaracoprimarygroup"""Native adapter for serving CherryPy via mod_python Basic usage: ########################################## # Application in a module called myapp.py ########################################## import cherrypy class Root: @cherrypy.expose def index(self): return 'Hi there, Ho there, Hey there' # We will use this method from the mod_python configuration # as the entry point to our application def setup_server(): cherrypy.tree.mount(Root()) cherrypy.config.update({'environment': 'production', 'log.screen': False, 'show_tracebacks': False}) ########################################## # mod_python settings for apache2 # This should reside in your httpd.conf # or a file that will be loaded at # apache startup ########################################## # Start DocumentRoot "/" Listen 8080 LoadModule python_module /usr/lib/apache2/modules/mod_python.so PythonPath "sys.path+['/path/to/my/application']" SetHandler python-program PythonHandler cherrypy._cpmodpy::handler PythonOption cherrypy.setup myapp::setup_server PythonDebug On # End The actual path to your mod_python.so is dependent on your environment. In this case we suppose a global mod_python installation on a Linux distribution such as Ubuntu. We do set the PythonPath configuration setting so that your application can be found by from the user running the apache2 instance. Of course if your application resides in the global site-package this won't be needed. Then restart apache2 and access http://127.0.0.1:8080 """ import io import logging import os import re import sys from more_itertools import always_iterable import cherrypy from cherrypy._cperror import format_exc, bare_error from cherrypy.lib import httputil # ------------------------------ Request-handling def setup(req): from mod_python import apache # Run any setup functions defined by a "PythonOption cherrypy.setup" # directive. options = req.get_options() if 'cherrypy.setup' in options: for function in options['cherrypy.setup'].split(): atoms = function.split('::', 1) if len(atoms) == 1: mod = __import__(atoms[0], globals(), locals()) else: modname, fname = atoms mod = __import__(modname, globals(), locals(), [fname]) func = getattr(mod, fname) func() cherrypy.config.update({'log.screen': False, 'tools.ignore_headers.on': True, 'tools.ignore_headers.headers': ['Range'], }) engine = cherrypy.engine if hasattr(engine, 'signal_handler'): engine.signal_handler.unsubscribe() if hasattr(engine, 'console_control_handler'): engine.console_control_handler.unsubscribe() engine.autoreload.unsubscribe() cherrypy.server.unsubscribe() @engine.subscribe('log') def _log(msg, level): newlevel = apache.APLOG_ERR if logging.DEBUG >= level: newlevel = apache.APLOG_DEBUG elif logging.INFO >= level: newlevel = apache.APLOG_INFO elif logging.WARNING >= level: newlevel = apache.APLOG_WARNING # On Windows, req.server is required or the msg will vanish. See # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html # Also, "When server is not specified...LogLevel does not apply..." apache.log_error(msg, newlevel, req.server) engine.start() def cherrypy_cleanup(data): engine.exit() try: # apache.register_cleanup wasn't available until 3.1.4. apache.register_cleanup(cherrypy_cleanup) except AttributeError: req.server.register_cleanup(req, cherrypy_cleanup) class _ReadOnlyRequest: expose = ('read', 'readline', 'readlines') def __init__(self, req): for method in self.expose: self.__dict__[method] = getattr(req, method) recursive = False _isSetUp = False def handler(req): from mod_python import apache try: global _isSetUp if not _isSetUp: setup(req) _isSetUp = True # Obtain a Request object from CherryPy local = req.connection.local_addr local = httputil.Host( local[0], local[1], req.connection.local_host or '') remote = req.connection.remote_addr remote = httputil.Host( remote[0], remote[1], req.connection.remote_host or '') scheme = req.parsed_uri[0] or 'http' req.get_basic_auth_pw() try: # apache.mpm_query only became available in mod_python 3.1 q = apache.mpm_query threaded = q(apache.AP_MPMQ_IS_THREADED) forked = q(apache.AP_MPMQ_IS_FORKED) except AttributeError: bad_value = ("You must provide a PythonOption '%s', " "either 'on' or 'off', when running a version " 'of mod_python < 3.1') options = req.get_options() threaded = options.get('multithread', '').lower() if threaded == 'on': threaded = True elif threaded == 'off': threaded = False else: raise ValueError(bad_value % 'multithread') forked = options.get('multiprocess', '').lower() if forked == 'on': forked = True elif forked == 'off': forked = False else: raise ValueError(bad_value % 'multiprocess') sn = cherrypy.tree.script_name(req.uri or '/') if sn is None: send_response(req, '404 Not Found', [], '') else: app = cherrypy.tree.apps[sn] method = req.method path = req.uri qs = req.args or '' reqproto = req.protocol headers = list(req.headers_in.copy().items()) rfile = _ReadOnlyRequest(req) prev = None try: redirections = [] while True: request, response = app.get_serving(local, remote, scheme, 'HTTP/1.1') request.login = req.user request.multithread = bool(threaded) request.multiprocess = bool(forked) request.app = app request.prev = prev # Run the CherryPy Request object and obtain the response try: request.run(method, path, qs, reqproto, headers, rfile) break except cherrypy.InternalRedirect: ir = sys.exc_info()[1] app.release_serving() prev = request if not recursive: if ir.path in redirections: raise RuntimeError( 'InternalRedirector visited the same URL ' 'twice: %r' % ir.path) else: # Add the *previous* path_info + qs to # redirections. if qs: qs = '?' + qs redirections.append(sn + path + qs) # Munge environment and try again. method = 'GET' path = ir.path qs = ir.query_string rfile = io.BytesIO() send_response( req, response.output_status, response.header_list, response.body, response.stream) finally: app.release_serving() except Exception: tb = format_exc() cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) s, h, b = bare_error() send_response(req, s, h, b) return apache.OK def send_response(req, status, headers, body, stream=False): # Set response status req.status = int(status[:3]) # Set response headers req.content_type = 'text/plain' for header, value in headers: if header.lower() == 'content-type': req.content_type = value continue req.headers_out.add(header, value) if stream: # Flush now so the status and headers are sent immediately. req.flush() # Set response body for seg in always_iterable(body): req.write(seg) # --------------- Startup tools for CherryPy + mod_python --------------- # try: import subprocess def popen(fullcmd): p = subprocess.Popen(fullcmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) return p.stdout except ImportError: def popen(fullcmd): pipein, pipeout = os.popen4(fullcmd) return pipeout def read_process(cmd, args=''): fullcmd = '%s %s' % (cmd, args) pipeout = popen(fullcmd) try: firstline = pipeout.readline() cmd_not_found = re.search( b'(not recognized|No such file|not found)', firstline, re.IGNORECASE ) if cmd_not_found: raise IOError('%s must be on your system path.' % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output class ModPythonServer(object): template = """ # Apache2 server configuration file for running CherryPy with mod_python. DocumentRoot "/" Listen %(port)s LoadModule python_module modules/mod_python.so SetHandler python-program PythonHandler %(handler)s PythonDebug On %(opts)s """ def __init__(self, loc='/', port=80, opts=None, apache_path='apache', handler='cherrypy._cpmodpy::handler'): self.loc = loc self.port = port self.opts = opts self.apache_path = apache_path self.handler = handler def start(self): opts = ''.join([' PythonOption %s %s\n' % (k, v) for k, v in self.opts]) conf_data = self.template % {'port': self.port, 'loc': self.loc, 'opts': opts, 'handler': self.handler, } mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') with open(mpconf, 'wb') as f: f.write(conf_data) response = read_process(self.apache_path, '-k start -f %s' % mpconf) self.ready = True return response def stop(self): os.popen('apache -k stop') self.ready = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpnative_server.py0000644252176402575230000001502514307416152022013 0ustar00jaracoprimarygroup"""Native adapter for serving CherryPy via its builtin server.""" import logging import sys import io import cheroot.server import cherrypy from cherrypy._cperror import format_exc, bare_error from cherrypy.lib import httputil from ._cpcompat import tonative class NativeGateway(cheroot.server.Gateway): """Native gateway implementation allowing to bypass WSGI.""" recursive = False def respond(self): """Obtain response from CherryPy machinery and then send it.""" req = self.req try: # Obtain a Request object from CherryPy local = req.server.bind_addr # FIXME: handle UNIX sockets local = tonative(local[0]), local[1] local = httputil.Host(local[0], local[1], '') remote = tonative(req.conn.remote_addr), req.conn.remote_port remote = httputil.Host(remote[0], remote[1], '') scheme = tonative(req.scheme) sn = cherrypy.tree.script_name(tonative(req.uri or '/')) if sn is None: self.send_response('404 Not Found', [], ['']) else: app = cherrypy.tree.apps[sn] method = tonative(req.method) path = tonative(req.path) qs = tonative(req.qs or '') headers = ( (tonative(h), tonative(v)) for h, v in req.inheaders.items() ) rfile = req.rfile prev = None try: redirections = [] while True: request, response = app.get_serving( local, remote, scheme, 'HTTP/1.1') request.multithread = True request.multiprocess = False request.app = app request.prev = prev # Run the CherryPy Request object and obtain the # response try: request.run( method, path, qs, tonative(req.request_protocol), headers, rfile, ) break except cherrypy.InternalRedirect: ir = sys.exc_info()[1] app.release_serving() prev = request if not self.recursive: if ir.path in redirections: raise RuntimeError( 'InternalRedirector visited the same ' 'URL twice: %r' % ir.path) else: # Add the *previous* path_info + qs to # redirections. if qs: qs = '?' + qs redirections.append(sn + path + qs) # Munge environment and try again. method = 'GET' path = ir.path qs = ir.query_string rfile = io.BytesIO() self.send_response( response.output_status, response.header_list, response.body) finally: app.release_serving() except Exception: tb = format_exc() # print tb cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) s, h, b = bare_error() self.send_response(s, h, b) def send_response(self, status, headers, body): """Send response to HTTP request.""" req = self.req # Set response status req.status = status or b'500 Server Error' # Set response headers for header, value in headers: req.outheaders.append((header, value)) if (req.ready and not req.sent_headers): req.sent_headers = True req.send_headers() # Set response body for seg in body: req.write(seg) class CPHTTPServer(cheroot.server.HTTPServer): """Wrapper for cheroot.server.HTTPServer. cheroot has been designed to not reference CherryPy in any way, so that it can be used in other frameworks and applications. Therefore, we wrap it here, so we can apply some attributes from config -> cherrypy.server -> HTTPServer. """ def __init__(self, server_adapter=cherrypy.server): """Initialize CPHTTPServer.""" self.server_adapter = server_adapter server_name = (self.server_adapter.socket_host or self.server_adapter.socket_file or None) cheroot.server.HTTPServer.__init__( self, server_adapter.bind_addr, NativeGateway, minthreads=server_adapter.thread_pool, maxthreads=server_adapter.thread_pool_max, server_name=server_name) self.max_request_header_size = ( self.server_adapter.max_request_header_size or 0) self.max_request_body_size = ( self.server_adapter.max_request_body_size or 0) self.request_queue_size = self.server_adapter.socket_queue_size self.timeout = self.server_adapter.socket_timeout self.shutdown_timeout = self.server_adapter.shutdown_timeout self.protocol = self.server_adapter.protocol_version self.nodelay = self.server_adapter.nodelay ssl_module = self.server_adapter.ssl_module or 'pyopenssl' if self.server_adapter.ssl_context: adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) self.ssl_adapter = adapter_class( self.server_adapter.ssl_certificate, self.server_adapter.ssl_private_key, self.server_adapter.ssl_certificate_chain, self.server_adapter.ssl_ciphers) self.ssl_adapter.context = self.server_adapter.ssl_context elif self.server_adapter.ssl_certificate: adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) self.ssl_adapter = adapter_class( self.server_adapter.ssl_certificate, self.server_adapter.ssl_private_key, self.server_adapter.ssl_certificate_chain, self.server_adapter.ssl_ciphers) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpreqbody.py0000644252176402575230000010703614307416152020610 0ustar00jaracoprimarygroup"""Request body processing for CherryPy. .. versionadded:: 3.2 Application authors have complete control over the parsing of HTTP request entities. In short, :attr:`cherrypy.request.body` is now always set to an instance of :class:`RequestBody`, and *that* class is a subclass of :class:`Entity`. When an HTTP request includes an entity body, it is often desirable to provide that information to applications in a form other than the raw bytes. Different content types demand different approaches. Examples: * For a GIF file, we want the raw bytes in a stream. * An HTML form is better parsed into its component fields, and each text field decoded from bytes to unicode. * A JSON body should be deserialized into a Python dict or list. When the request contains a Content-Type header, the media type is used as a key to look up a value in the :attr:`request.body.processors` dict. If the full media type is not found, then the major type is tried; for example, if no processor is found for the 'image/jpeg' type, then we look for a processor for the 'image' types altogether. If neither the full type nor the major type has a matching processor, then a default processor is used (:func:`default_proc`). For most types, this means no processing is done, and the body is left unread as a raw byte stream. Processors are configurable in an 'on_start_resource' hook. Some processors, especially those for the 'text' types, attempt to decode bytes to unicode. If the Content-Type request header includes a 'charset' parameter, this is used to decode the entity. Otherwise, one or more default charsets may be attempted, although this decision is up to each processor. If a processor successfully decodes an Entity or Part, it should set the :attr:`charset` attribute on the Entity or Part to the name of the successful charset, so that applications can easily re-encode or transcode the value if they wish. If the Content-Type of the request entity is of major type 'multipart', then the above parsing process, and possibly a decoding process, is performed for each part. For both the full entity and multipart parts, a Content-Disposition header may be used to fill :attr:`name` and :attr:`filename` attributes on the request.body or the Part. .. _custombodyprocessors: Custom Processors ================= You can add your own processors for any specific or major MIME type. Simply add it to the :attr:`processors` dict in a hook/tool that runs at ``on_start_resource`` or ``before_request_body``. Here's the built-in JSON tool for an example:: def json_in(force=True, debug=False): request = cherrypy.serving.request def json_processor(entity): '''Read application/json data into request.json.''' if not entity.headers.get("Content-Length", ""): raise cherrypy.HTTPError(411) body = entity.fp.read() try: request.json = json_decode(body) except ValueError: raise cherrypy.HTTPError(400, 'Invalid JSON document') if force: request.body.processors.clear() request.body.default_proc = cherrypy.HTTPError( 415, 'Expected an application/json content type') request.body.processors['application/json'] = json_processor We begin by defining a new ``json_processor`` function to stick in the ``processors`` dictionary. All processor functions take a single argument, the ``Entity`` instance they are to process. It will be called whenever a request is received (for those URI's where the tool is turned on) which has a ``Content-Type`` of "application/json". First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then reads the remaining bytes on the socket. The ``fp`` object knows its own length, so it won't hang waiting for data that never arrives. It will return when all data has been read. Then, we decode those bytes using Python's built-in ``json`` module, and stick the decoded result onto ``request.json`` . If it cannot be decoded, we raise 400. If the "force" argument is True (the default), the ``Tool`` clears the ``processors`` dict so that request entities of other ``Content-Types`` aren't parsed at all. Since there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body`` is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.) But in our case, we want to raise 415, so we replace ``request.body.default_proc`` with the error (``HTTPError`` instances, when called, raise themselves). If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry:: request.body.processors = {'application/json': json_processor} Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one. """ try: from io import DEFAULT_BUFFER_SIZE except ImportError: DEFAULT_BUFFER_SIZE = 8192 import re import sys import tempfile from urllib.parse import unquote import cheroot.server import cherrypy from cherrypy._cpcompat import ntou from cherrypy.lib import httputil def unquote_plus(bs): """Bytes version of urllib.parse.unquote_plus.""" bs = bs.replace(b'+', b' ') atoms = bs.split(b'%') for i in range(1, len(atoms)): item = atoms[i] try: pct = int(item[:2], 16) atoms[i] = bytes([pct]) + item[2:] except ValueError: pass return b''.join(atoms) # ------------------------------- Processors -------------------------------- # def process_urlencoded(entity): """Read application/x-www-form-urlencoded data into entity.params.""" qs = entity.fp.read() for charset in entity.attempt_charsets: try: params = {} for aparam in qs.split(b'&'): for pair in aparam.split(b';'): if not pair: continue atoms = pair.split(b'=', 1) if len(atoms) == 1: atoms.append(b'') key = unquote_plus(atoms[0]).decode(charset) value = unquote_plus(atoms[1]).decode(charset) if key in params: if not isinstance(params[key], list): params[key] = [params[key]] params[key].append(value) else: params[key] = value except UnicodeDecodeError: pass else: entity.charset = charset break else: raise cherrypy.HTTPError( 400, 'The request entity could not be decoded. The following ' 'charsets were attempted: %s' % repr(entity.attempt_charsets)) # Now that all values have been successfully parsed and decoded, # apply them to the entity.params dict. for key, value in params.items(): if key in entity.params: if not isinstance(entity.params[key], list): entity.params[key] = [entity.params[key]] entity.params[key].append(value) else: entity.params[key] = value def process_multipart(entity): """Read all multipart parts into entity.parts.""" ib = '' if 'boundary' in entity.content_type.params: # http://tools.ietf.org/html/rfc2046#section-5.1.1 # "The grammar for parameters on the Content-type field is such that it # is often necessary to enclose the boundary parameter values in quotes # on the Content-type line" ib = entity.content_type.params['boundary'].strip('"') if not re.match('^[ -~]{0,200}[!-~]$', ib): raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) ib = ('--' + ib).encode('ascii') # Find the first marker while True: b = entity.readline() if not b: return b = b.strip() if b == ib: break # Read all parts while True: part = entity.part_class.from_fp(entity.fp, ib) entity.parts.append(part) part.process() if part.fp.done: break def process_multipart_form_data(entity): """Read all multipart/form-data parts into entity.parts or entity.params. """ process_multipart(entity) kept_parts = [] for part in entity.parts: if part.name is None: kept_parts.append(part) else: if part.filename is None: # It's a regular field value = part.fullvalue() else: # It's a file upload. Retain the whole part so consumer code # has access to its .file and .filename attributes. value = part if part.name in entity.params: if not isinstance(entity.params[part.name], list): entity.params[part.name] = [entity.params[part.name]] entity.params[part.name].append(value) else: entity.params[part.name] = value entity.parts = kept_parts def _old_process_multipart(entity): """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" process_multipart(entity) params = entity.params for part in entity.parts: if part.name is None: key = ntou('parts') else: key = part.name if part.filename is None: # It's a regular field value = part.fullvalue() else: # It's a file upload. Retain the whole part so consumer code # has access to its .file and .filename attributes. value = part if key in params: if not isinstance(params[key], list): params[key] = [params[key]] params[key].append(value) else: params[key] = value # -------------------------------- Entities --------------------------------- # class Entity(object): """An HTTP request body, or MIME multipart body. This class collects information about the HTTP request entity. When a given entity is of MIME type "multipart", each part is parsed into its own Entity instance, and the set of parts stored in :attr:`entity.parts`. Between the ``before_request_body`` and ``before_handler`` tools, CherryPy tries to process the request body (if any) by calling :func:`request.body.process`. This uses the ``content_type`` of the Entity to look up a suitable processor in :attr:`Entity.processors`, a dict. If a matching processor cannot be found for the complete Content-Type, it tries again using the major type. For example, if a request with an entity of type "image/jpeg" arrives, but no processor can be found for that complete type, then one is sought for the major type "image". If a processor is still not found, then the :func:`default_proc` method of the Entity is called (which does nothing by default; you can override this too). CherryPy includes processors for the "application/x-www-form-urlencoded" type, the "multipart/form-data" type, and the "multipart" major type. CherryPy 3.2 processes these types almost exactly as older versions. Parts are passed as arguments to the page handler using their ``Content-Disposition.name`` if given, otherwise in a generic "parts" argument. Each such part is either a string, or the :class:`Part` itself if it's a file. (In this case it will have ``file`` and ``filename`` attributes, or possibly a ``value`` attribute). Each Part is itself a subclass of Entity, and has its own ``process`` method and ``processors`` dict. There is a separate processor for the "multipart" major type which is more flexible, and simply stores all multipart parts in :attr:`request.body.parts`. You can enable it with:: cherrypy.request.body.processors['multipart'] = \ _cpreqbody.process_multipart in an ``on_start_resource`` tool. """ # http://tools.ietf.org/html/rfc2046#section-4.1.2: # "The default character set, which must be assumed in the # absence of a charset parameter, is US-ASCII." # However, many browsers send data in utf-8 with no charset. attempt_charsets = ['utf-8'] r"""A list of strings, each of which should be a known encoding. When the Content-Type of the request body warrants it, each of the given encodings will be tried in order. The first one to successfully decode the entity without raising an error is stored as :attr:`entity.charset`. This defaults to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by `HTTP/1.1 `_), but ``['us-ascii', 'utf-8']`` for multipart parts. """ charset = None """The successful decoding; see "attempt_charsets" above.""" content_type = None """The value of the Content-Type request header. If the Entity is part of a multipart payload, this will be the Content-Type given in the MIME headers for this part. """ default_content_type = 'application/x-www-form-urlencoded' """This defines a default ``Content-Type`` to use if no Content-Type header is given. The empty string is used for RequestBody, which results in the request body not being read or parsed at all. This is by design; a missing ``Content-Type`` header in the HTTP request entity is an error at best, and a security hole at worst. For multipart parts, however, the MIME spec declares that a part with no Content-Type defaults to "text/plain" (see :class:`Part`). """ filename = None """The ``Content-Disposition.filename`` header, if available.""" fp = None """The readable socket file object.""" headers = None """A dict of request/multipart header names and values. This is a copy of the ``request.headers`` for the ``request.body``; for multipart parts, it is the set of headers for that part. """ length = None """The value of the ``Content-Length`` header, if provided.""" name = None """The "name" parameter of the ``Content-Disposition`` header, if any.""" params = None """ If the request Content-Type is 'application/x-www-form-urlencoded' or multipart, this will be a dict of the params pulled from the entity body; that is, it will be the portion of request.params that come from the message body (sometimes called "POST params", although they can be sent with various HTTP method verbs). This value is set between the 'before_request_body' and 'before_handler' hooks (assuming that process_request_body is True).""" processors = {'application/x-www-form-urlencoded': process_urlencoded, 'multipart/form-data': process_multipart_form_data, 'multipart': process_multipart, } """A dict of Content-Type names to processor methods.""" parts = None """A list of Part instances if ``Content-Type`` is of major type "multipart".""" part_class = None """The class used for multipart parts. You can replace this with custom subclasses to alter the processing of multipart parts. """ def __init__(self, fp, headers, params=None, parts=None): # Make an instance-specific copy of the class processors # so Tools, etc. can replace them per-request. self.processors = self.processors.copy() self.fp = fp self.headers = headers if params is None: params = {} self.params = params if parts is None: parts = [] self.parts = parts # Content-Type self.content_type = headers.elements('Content-Type') if self.content_type: self.content_type = self.content_type[0] else: self.content_type = httputil.HeaderElement.from_str( self.default_content_type) # Copy the class 'attempt_charsets', prepending any Content-Type # charset dec = self.content_type.params.get('charset', None) if dec: self.attempt_charsets = [dec] + [c for c in self.attempt_charsets if c != dec] else: self.attempt_charsets = self.attempt_charsets[:] # Length self.length = None clen = headers.get('Content-Length', None) # If Transfer-Encoding is 'chunked', ignore any Content-Length. if ( clen is not None and 'chunked' not in headers.get('Transfer-Encoding', '') ): try: self.length = int(clen) except ValueError: pass # Content-Disposition self.name = None self.filename = None disp = headers.elements('Content-Disposition') if disp: disp = disp[0] if 'name' in disp.params: self.name = disp.params['name'] if self.name.startswith('"') and self.name.endswith('"'): self.name = self.name[1:-1] if 'filename' in disp.params: self.filename = disp.params['filename'] if ( self.filename.startswith('"') and self.filename.endswith('"') ): self.filename = self.filename[1:-1] if 'filename*' in disp.params: # @see https://tools.ietf.org/html/rfc5987 encoding, lang, filename = disp.params['filename*'].split("'") self.filename = unquote(str(filename), encoding) def read(self, size=None, fp_out=None): return self.fp.read(size, fp_out) def readline(self, size=None): return self.fp.readline(size) def readlines(self, sizehint=None): return self.fp.readlines(sizehint) def __iter__(self): return self def __next__(self): line = self.readline() if not line: raise StopIteration return line def next(self): return self.__next__() def read_into_file(self, fp_out=None): """Read the request body into fp_out (or make_file() if None). Return fp_out. """ if fp_out is None: fp_out = self.make_file() self.read(fp_out=fp_out) return fp_out def make_file(self): """Return a file-like object into which the request body will be read. By default, this will return a TemporaryFile. Override as needed. See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" return tempfile.TemporaryFile() def fullvalue(self): """Return this entity as a string, whether stored in a file or not.""" if self.file: # It was stored in a tempfile. Read it. self.file.seek(0) value = self.file.read() self.file.seek(0) else: value = self.value value = self.decode_entity(value) return value def decode_entity(self, value): """Return a given byte encoded value as a string""" for charset in self.attempt_charsets: try: value = value.decode(charset) except UnicodeDecodeError: pass else: self.charset = charset return value else: raise cherrypy.HTTPError( 400, 'The request entity could not be decoded. The following ' 'charsets were attempted: %s' % repr(self.attempt_charsets) ) def process(self): """Execute the best-match processor for the given media type.""" proc = None ct = self.content_type.value try: proc = self.processors[ct] except KeyError: toptype = ct.split('/', 1)[0] try: proc = self.processors[toptype] except KeyError: pass if proc is None: self.default_proc() else: proc(self) def default_proc(self): """Called if a more-specific processor is not found for the ``Content-Type``. """ # Leave the fp alone for someone else to read. This works fine # for request.body, but the Part subclasses need to override this # so they can move on to the next part. pass class Part(Entity): """A MIME part entity, part of a multipart entity.""" # "The default character set, which must be assumed in the absence of a # charset parameter, is US-ASCII." attempt_charsets = ['us-ascii', 'utf-8'] r"""A list of strings, each of which should be a known encoding. When the Content-Type of the request body warrants it, each of the given encodings will be tried in order. The first one to successfully decode the entity without raising an error is stored as :attr:`entity.charset`. This defaults to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by `HTTP/1.1 `_), but ``['us-ascii', 'utf-8']`` for multipart parts. """ boundary = None """The MIME multipart boundary.""" default_content_type = 'text/plain' """This defines a default ``Content-Type`` to use if no Content-Type header is given. The empty string is used for RequestBody, which results in the request body not being read or parsed at all. This is by design; a missing ``Content-Type`` header in the HTTP request entity is an error at best, and a security hole at worst. For multipart parts, however (this class), the MIME spec declares that a part with no Content-Type defaults to "text/plain". """ # This is the default in stdlib cgi. We may want to increase it. maxrambytes = 1000 """The threshold of bytes after which point the ``Part`` will store its data in a file (generated by :func:`make_file`) instead of a string. Defaults to 1000, just like the :mod:`cgi` module in Python's standard library. """ def __init__(self, fp, headers, boundary): Entity.__init__(self, fp, headers) self.boundary = boundary self.file = None self.value = None @classmethod def from_fp(cls, fp, boundary): headers = cls.read_headers(fp) return cls(fp, headers, boundary) @classmethod def read_headers(cls, fp): headers = httputil.HeaderMap() while True: line = fp.readline() if not line: # No more data--illegal end of headers raise EOFError('Illegal end of headers.') if line == b'\r\n': # Normal end of headers break if not line.endswith(b'\r\n'): raise ValueError('MIME requires CRLF terminators: %r' % line) if line[0] in b' \t': # It's a continuation line. v = line.strip().decode('ISO-8859-1') else: k, v = line.split(b':', 1) k = k.strip().decode('ISO-8859-1') v = v.strip().decode('ISO-8859-1') existing = headers.get(k) if existing: v = ', '.join((existing, v)) headers[k] = v return headers def read_lines_to_boundary(self, fp_out=None): """Read bytes from self.fp and return or write them to a file. If the 'fp_out' argument is None (the default), all bytes read are returned in a single byte string. If the 'fp_out' argument is not None, it must be a file-like object that supports the 'write' method; all bytes read will be written to the fp, and that fp is returned. """ endmarker = self.boundary + b'--' delim = b'' prev_lf = True lines = [] seen = 0 while True: line = self.fp.readline(1 << 16) if not line: raise EOFError('Illegal end of multipart body.') if line.startswith(b'--') and prev_lf: strippedline = line.strip() if strippedline == self.boundary: break if strippedline == endmarker: self.fp.finish() break line = delim + line if line.endswith(b'\r\n'): delim = b'\r\n' line = line[:-2] prev_lf = True elif line.endswith(b'\n'): delim = b'\n' line = line[:-1] prev_lf = True else: delim = b'' prev_lf = False if fp_out is None: lines.append(line) seen += len(line) if seen > self.maxrambytes: fp_out = self.make_file() for line in lines: fp_out.write(line) else: fp_out.write(line) if fp_out is None: result = b''.join(lines) return result else: fp_out.seek(0) return fp_out def default_proc(self): """Called if a more-specific processor is not found for the ``Content-Type``. """ if self.filename: # Always read into a file if a .filename was given. self.file = self.read_into_file() else: result = self.read_lines_to_boundary() if isinstance(result, bytes): self.value = result else: self.file = result def read_into_file(self, fp_out=None): """Read the request body into fp_out (or make_file() if None). Return fp_out. """ if fp_out is None: fp_out = self.make_file() self.read_lines_to_boundary(fp_out=fp_out) return fp_out Entity.part_class = Part inf = float('inf') class SizedReader: def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False): # Wrap our fp in a buffer so peek() works self.fp = fp self.length = length self.maxbytes = maxbytes self.buffer = b'' self.bufsize = bufsize self.bytes_read = 0 self.done = False self.has_trailers = has_trailers def read(self, size=None, fp_out=None): """Read bytes from the request body and return or write them to a file. A number of bytes less than or equal to the 'size' argument are read off the socket. The actual number of bytes read are tracked in self.bytes_read. The number may be smaller than 'size' when 1) the client sends fewer bytes, 2) the 'Content-Length' request header specifies fewer bytes than requested, or 3) the number of bytes read exceeds self.maxbytes (in which case, 413 is raised). If the 'fp_out' argument is None (the default), all bytes read are returned in a single byte string. If the 'fp_out' argument is not None, it must be a file-like object that supports the 'write' method; all bytes read will be written to the fp, and None is returned. """ if self.length is None: if size is None: remaining = inf else: remaining = size else: remaining = self.length - self.bytes_read if size and size < remaining: remaining = size if remaining == 0: self.finish() if fp_out is None: return b'' else: return None chunks = [] # Read bytes from the buffer. if self.buffer: if remaining is inf: data = self.buffer self.buffer = b'' else: data = self.buffer[:remaining] self.buffer = self.buffer[remaining:] datalen = len(data) remaining -= datalen # Check lengths. self.bytes_read += datalen if self.maxbytes and self.bytes_read > self.maxbytes: raise cherrypy.HTTPError(413) # Store the data. if fp_out is None: chunks.append(data) else: fp_out.write(data) # Read bytes from the socket. while remaining > 0: chunksize = min(remaining, self.bufsize) try: data = self.fp.read(chunksize) except Exception: e = sys.exc_info()[1] if e.__class__.__name__ == 'MaxSizeExceeded': # Post data is too big raise cherrypy.HTTPError( 413, 'Maximum request length: %r' % e.args[1]) else: raise if not data: self.finish() break datalen = len(data) remaining -= datalen # Check lengths. self.bytes_read += datalen if self.maxbytes and self.bytes_read > self.maxbytes: raise cherrypy.HTTPError(413) # Store the data. if fp_out is None: chunks.append(data) else: fp_out.write(data) if fp_out is None: return b''.join(chunks) def readline(self, size=None): """Read a line from the request body and return it.""" chunks = [] while size is None or size > 0: chunksize = self.bufsize if size is not None and size < self.bufsize: chunksize = size data = self.read(chunksize) if not data: break pos = data.find(b'\n') + 1 if pos: chunks.append(data[:pos]) remainder = data[pos:] self.buffer += remainder self.bytes_read -= len(remainder) break else: chunks.append(data) return b''.join(chunks) def readlines(self, sizehint=None): """Read lines from the request body and return them.""" if self.length is not None: if sizehint is None: sizehint = self.length - self.bytes_read else: sizehint = min(sizehint, self.length - self.bytes_read) lines = [] seen = 0 while True: line = self.readline() if not line: break lines.append(line) seen += len(line) if seen >= sizehint: break return lines def finish(self): self.done = True if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): self.trailers = {} try: for line in self.fp.read_trailer_lines(): if line[0] in b' \t': # It's a continuation line. v = line.strip() else: try: k, v = line.split(b':', 1) except ValueError: raise ValueError('Illegal header line.') k = k.strip().title() v = v.strip() if k in cheroot.server.comma_separated_headers: existing = self.trailers.get(k) if existing: v = b', '.join((existing, v)) self.trailers[k] = v except Exception: e = sys.exc_info()[1] if e.__class__.__name__ == 'MaxSizeExceeded': # Post data is too big raise cherrypy.HTTPError( 413, 'Maximum request length: %r' % e.args[1]) else: raise class RequestBody(Entity): """The entity of the HTTP request.""" bufsize = 8 * 1024 """The buffer size used when reading the socket.""" # Don't parse the request body at all if the client didn't provide # a Content-Type header. See # https://github.com/cherrypy/cherrypy/issues/790 default_content_type = '' """This defines a default ``Content-Type`` to use if no Content-Type header is given. The empty string is used for RequestBody, which results in the request body not being read or parsed at all. This is by design; a missing ``Content-Type`` header in the HTTP request entity is an error at best, and a security hole at worst. For multipart parts, however, the MIME spec declares that a part with no Content-Type defaults to "text/plain" (see :class:`Part`). """ maxbytes = None """Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket. """ def __init__(self, fp, headers, params=None, request_params=None): Entity.__init__(self, fp, headers, params) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 # When no explicit charset parameter is provided by the # sender, media subtypes of the "text" type are defined # to have a default charset value of "ISO-8859-1" when # received via HTTP. if self.content_type.value.startswith('text/'): for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): if c in self.attempt_charsets: break else: self.attempt_charsets.append('ISO-8859-1') # Temporary fix while deprecating passing .parts as .params. self.processors['multipart'] = _old_process_multipart if request_params is None: request_params = {} self.request_params = request_params def process(self): """Process the request entity based on its Content-Type.""" # "The presence of a message-body in a request is signaled by the # inclusion of a Content-Length or Transfer-Encoding header field in # the request's message-headers." # It is possible to send a POST request with no body, for example; # however, app developers are responsible in that case to set # cherrypy.request.process_body to False so this method isn't called. h = cherrypy.serving.request.headers if 'Content-Length' not in h and 'Transfer-Encoding' not in h: raise cherrypy.HTTPError(411) self.fp = SizedReader(self.fp, self.length, self.maxbytes, bufsize=self.bufsize, has_trailers='Trailer' in h) super(RequestBody, self).process() # Body params should also be a part of the request_params # add them in here. request_params = self.request_params for key, value in self.params.items(): if key in request_params: if not isinstance(request_params[key], list): request_params[key] = [request_params[key]] request_params[key].append(value) else: request_params[key] = value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cprequest.py0000644252176402575230000010315014307416152020624 0ustar00jaracoprimarygroupimport sys import time import collections import operator from http.cookies import SimpleCookie, CookieError import uuid from more_itertools import consume import cherrypy from cherrypy._cpcompat import ntob from cherrypy import _cpreqbody from cherrypy._cperror import format_exc, bare_error from cherrypy.lib import httputil, reprconf, encoding class Hook(object): """A callback and its metadata: failsafe, priority, and kwargs.""" callback = None """ The bare callable that this Hook object is wrapping, which will be called when the Hook is called.""" failsafe = False """ If True, the callback is guaranteed to run even if other callbacks from the same call point raise exceptions.""" priority = 50 """ Defines the order of execution for a list of Hooks. Priority numbers should be limited to the closed interval [0, 100], but values outside this range are acceptable, as are fractional values.""" kwargs = {} """ A set of keyword arguments that will be passed to the callable on each call.""" def __init__(self, callback, failsafe=None, priority=None, **kwargs): self.callback = callback if failsafe is None: failsafe = getattr(callback, 'failsafe', False) self.failsafe = failsafe if priority is None: priority = getattr(callback, 'priority', 50) self.priority = priority self.kwargs = kwargs def __lt__(self, other): """ Hooks sort by priority, ascending, such that hooks of lower priority are run first. """ return self.priority < other.priority def __call__(self): """Run self.callback(**self.kwargs).""" return self.callback(**self.kwargs) def __repr__(self): cls = self.__class__ return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)' % (cls.__module__, cls.__name__, self.callback, self.failsafe, self.priority, ', '.join(['%s=%r' % (k, v) for k, v in self.kwargs.items()]))) class HookMap(dict): """A map of call points to lists of callbacks (Hook objects).""" def __new__(cls, points=None): d = dict.__new__(cls) for p in points or []: d[p] = [] return d def __init__(self, *a, **kw): pass def attach(self, point, callback, failsafe=None, priority=None, **kwargs): """Append a new Hook made from the supplied arguments.""" self[point].append(Hook(callback, failsafe, priority, **kwargs)) def run(self, point): """Execute all registered Hooks (callbacks) for the given point.""" self.run_hooks(iter(sorted(self[point]))) @classmethod def run_hooks(cls, hooks): """Execute the indicated hooks, trapping errors. Hooks with ``.failsafe == True`` are guaranteed to run even if others at the same hookpoint fail. In this case, log the failure and proceed on to the next hook. The only way to stop all processing from one of these hooks is to raise a BaseException like SystemExit or KeyboardInterrupt and stop the whole server. """ assert isinstance(hooks, collections.abc.Iterator) quiet_errors = ( cherrypy.HTTPError, cherrypy.HTTPRedirect, cherrypy.InternalRedirect, ) safe = filter(operator.attrgetter('failsafe'), hooks) for hook in hooks: try: hook() except quiet_errors: cls.run_hooks(safe) raise except Exception: cherrypy.log(traceback=True, severity=40) cls.run_hooks(safe) raise def __copy__(self): newmap = self.__class__() # We can't just use 'update' because we want copies of the # mutable values (each is a list) as well. for k, v in self.items(): newmap[k] = v[:] return newmap copy = __copy__ def __repr__(self): cls = self.__class__ return '%s.%s(points=%r)' % ( cls.__module__, cls.__name__, list(self) ) # Config namespace handlers def hooks_namespace(k, v): """Attach bare hooks declared in config.""" # Use split again to allow multiple hooks for a single # hookpoint per path (e.g. "hooks.before_handler.1"). # Little-known fact you only get from reading source ;) hookpoint = k.split('.', 1)[0] if isinstance(v, str): v = cherrypy.lib.reprconf.attributes(v) if not isinstance(v, Hook): v = Hook(v) cherrypy.serving.request.hooks[hookpoint].append(v) def request_namespace(k, v): """Attach request attributes declared in config.""" # Provides config entries to set request.body attrs (like # attempt_charsets). if k[:5] == 'body.': setattr(cherrypy.serving.request.body, k[5:], v) else: setattr(cherrypy.serving.request, k, v) def response_namespace(k, v): """Attach response attributes declared in config.""" # Provides config entries to set default response headers # http://cherrypy.dev/ticket/889 if k[:8] == 'headers.': cherrypy.serving.response.headers[k.split('.', 1)[1]] = v else: setattr(cherrypy.serving.response, k, v) def error_page_namespace(k, v): """Attach error pages declared in config.""" if k != 'default': k = int(k) cherrypy.serving.request.error_page[k] = v hookpoints = ['on_start_resource', 'before_request_body', 'before_handler', 'before_finalize', 'on_end_resource', 'on_end_request', 'before_error_response', 'after_error_response'] class Request(object): """An HTTP request. This object represents the metadata of an HTTP request message; that is, it contains attributes which describe the environment in which the request URL, headers, and body were sent (if you want tools to interpret the headers and body, those are elsewhere, mostly in Tools). This 'metadata' consists of socket data, transport characteristics, and the Request-Line. This object also contains data regarding the configuration in effect for the given URL, and the execution plan for generating a response. """ prev = None """ The previous Request object (if any). This should be None unless we are processing an InternalRedirect.""" # Conversation/connection attributes local = httputil.Host('127.0.0.1', 80) 'An httputil.Host(ip, port, hostname) object for the server socket.' remote = httputil.Host('127.0.0.1', 1111) 'An httputil.Host(ip, port, hostname) object for the client socket.' scheme = 'http' """ The protocol used between client and server. In most cases, this will be either 'http' or 'https'.""" server_protocol = 'HTTP/1.1' """ The HTTP version for which the HTTP server is at least conditionally compliant.""" base = '' """The (scheme://host) portion of the requested URL. In some cases (e.g. when proxying via mod_rewrite), this may contain path segments which cherrypy.url uses when constructing url's, but which otherwise are ignored by CherryPy. Regardless, this value MUST NOT end in a slash.""" # Request-Line attributes request_line = '' """ The complete Request-Line received from the client. This is a single string consisting of the request method, URI, and protocol version (joined by spaces). Any final CRLF is removed.""" method = 'GET' """ Indicates the HTTP method to be performed on the resource identified by the Request-URI. Common methods include GET, HEAD, POST, PUT, and DELETE. CherryPy allows any extension method; however, various HTTP servers and gateways may restrict the set of allowable methods. CherryPy applications SHOULD restrict the set (on a per-URI basis).""" query_string = '' """ The query component of the Request-URI, a string of information to be interpreted by the resource. The query portion of a URI follows the path component, and is separated by a '?'. For example, the URI 'http://www.cherrypy.dev/wiki?a=3&b=4' has the query component, 'a=3&b=4'.""" query_string_encoding = 'utf8' """ The encoding expected for query string arguments after % HEX HEX decoding). If a query string is provided that cannot be decoded with this encoding, 404 is raised (since technically it's a different URI). If you want arbitrary encodings to not error, set this to 'Latin-1'; you can then encode back to bytes and re-decode to whatever encoding you like later. """ protocol = (1, 1) """The HTTP protocol version corresponding to the set of features which should be allowed in the response. If BOTH the client's request message AND the server's level of HTTP compliance is HTTP/1.1, this attribute will be the tuple (1, 1). If either is 1.0, this attribute will be the tuple (1, 0). Lower HTTP protocol versions are not explicitly supported.""" params = {} """ A dict which combines query string (GET) and request entity (POST) variables. This is populated in two stages: GET params are added before the 'on_start_resource' hook, and POST params are added between the 'before_request_body' and 'before_handler' hooks.""" # Message attributes header_list = [] """ A list of the HTTP request headers as (name, value) tuples. In general, you should use request.headers (a dict) instead.""" headers = httputil.HeaderMap() """ A dict-like object containing the request headers. Keys are header names (in Title-Case format); however, you may get and set them in a case-insensitive manner. That is, headers['Content-Type'] and headers['content-type'] refer to the same value. Values are header values (decoded according to :rfc:`2047` if necessary). See also: httputil.HeaderMap, httputil.HeaderElement.""" cookie = SimpleCookie() """See help(Cookie).""" rfile = None """ If the request included an entity (body), it will be available as a stream in this attribute. However, the rfile will normally be read for you between the 'before_request_body' hook and the 'before_handler' hook, and the resulting string is placed into either request.params or the request.body attribute. You may disable the automatic consumption of the rfile by setting request.process_request_body to False, either in config for the desired path, or in an 'on_start_resource' or 'before_request_body' hook. WARNING: In almost every case, you should not attempt to read from the rfile stream after CherryPy's automatic mechanism has read it. If you turn off the automatic parsing of rfile, you should read exactly the number of bytes specified in request.headers['Content-Length']. Ignoring either of these warnings may result in a hung request thread or in corruption of the next (pipelined) request. """ process_request_body = True """ If True, the rfile (if any) is automatically read and parsed, and the result placed into request.params or request.body.""" methods_with_bodies = ('POST', 'PUT', 'PATCH') """ A sequence of HTTP methods for which CherryPy will automatically attempt to read a body from the rfile. If you are going to change this property, modify it on the configuration (recommended) or on the "hook point" `on_start_resource`. """ body = None """ If the request Content-Type is 'application/x-www-form-urlencoded' or multipart, this will be None. Otherwise, this will be an instance of :class:`RequestBody` (which you can .read()); this value is set between the 'before_request_body' and 'before_handler' hooks (assuming that process_request_body is True).""" # Dispatch attributes dispatch = cherrypy.dispatch.Dispatcher() """ The object which looks up the 'page handler' callable and collects config for the current request based on the path_info, other request attributes, and the application architecture. The core calls the dispatcher as early as possible, passing it a 'path_info' argument. The default dispatcher discovers the page handler by matching path_info to a hierarchical arrangement of objects, starting at request.app.root. See help(cherrypy.dispatch) for more information.""" script_name = '' """ The 'mount point' of the application which is handling this request. This attribute MUST NOT end in a slash. If the script_name refers to the root of the URI, it MUST be an empty string (not "/"). """ path_info = '/' """ The 'relative path' portion of the Request-URI. This is relative to the script_name ('mount point') of the application which is handling this request.""" login = None """ When authentication is used during the request processing this is set to 'False' if it failed and to the 'username' value if it succeeded. The default 'None' implies that no authentication happened.""" # Note that cherrypy.url uses "if request.app:" to determine whether # the call is during a real HTTP request or not. So leave this None. app = None """The cherrypy.Application object which is handling this request.""" handler = None """ The function, method, or other callable which CherryPy will call to produce the response. The discovery of the handler and the arguments it will receive are determined by the request.dispatch object. By default, the handler is discovered by walking a tree of objects starting at request.app.root, and is then passed all HTTP params (from the query string and POST body) as keyword arguments.""" toolmaps = {} """ A nested dict of all Toolboxes and Tools in effect for this request, of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" config = None """ A flat dict of all configuration entries which apply to the current request. These entries are collected from global config, application config (based on request.path_info), and from handler config (exactly how is governed by the request.dispatch object in effect for this request; by default, handler config can be attached anywhere in the tree between request.app.root and the final handler, and inherits downward).""" is_index = None """ This will be True if the current request is mapped to an 'index' resource handler (also, a 'default' handler if path_info ends with a slash). The value may be used to automatically redirect the user-agent to a 'more canonical' URL which either adds or removes the trailing slash. See cherrypy.tools.trailing_slash.""" hooks = HookMap(hookpoints) """ A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. Each key is a str naming the hook point, and each value is a list of hooks which will be called at that hook point during this request. The list of hooks is generally populated as early as possible (mostly from Tools specified in config), but may be extended at any time. See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" error_response = cherrypy.HTTPError(500).set_response """ The no-arg callable which will handle unexpected, untrapped errors during request processing. This is not used for expected exceptions (like NotFound, HTTPError, or HTTPRedirect) which are raised in response to expected conditions (those should be customized either via request.error_page or by overriding HTTPError.set_response). By default, error_response uses HTTPError(500) to return a generic error response to the user-agent.""" error_page = {} """ A dict of {error code: response filename or callable} pairs. The error code must be an int representing a given HTTP error code, or the string 'default', which will be used if no matching entry is found for a given numeric code. If a filename is provided, the file should contain a Python string- formatting template, and can expect by default to receive format values with the mapping keys %(status)s, %(message)s, %(traceback)s, and %(version)s. The set of format mappings can be extended by overriding HTTPError.set_response. If a callable is provided, it will be called by default with keyword arguments 'status', 'message', 'traceback', and 'version', as for a string-formatting template. The callable must return a string or iterable of strings which will be set to response.body. It may also override headers or perform any other processing. If no entry is given for an error code, and no 'default' entry exists, a default template will be used. """ show_tracebacks = True """ If True, unexpected errors encountered during request processing will include a traceback in the response body.""" show_mismatched_params = True """ If True, mismatched parameters encountered during PageHandler invocation processing will be included in the response body.""" throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) """The sequence of exceptions which Request.run does not trap.""" throw_errors = False """ If True, Request.run will not trap any errors (except HTTPRedirect and HTTPError, which are more properly called 'exceptions', not errors).""" closed = False """True once the close method has been called, False otherwise.""" stage = None """ A string containing the stage reached in the request-handling process. This is useful when debugging a live server with hung requests.""" unique_id = None """A lazy object generating and memorizing UUID4 on ``str()`` render.""" namespaces = reprconf.NamespaceSet( **{'hooks': hooks_namespace, 'request': request_namespace, 'response': response_namespace, 'error_page': error_page_namespace, 'tools': cherrypy.tools, }) def __init__(self, local_host, remote_host, scheme='http', server_protocol='HTTP/1.1'): """Populate a new Request object. local_host should be an httputil.Host object with the server info. remote_host should be an httputil.Host object with the client info. scheme should be a string, either "http" or "https". """ self.local = local_host self.remote = remote_host self.scheme = scheme self.server_protocol = server_protocol self.closed = False # Put a *copy* of the class error_page into self. self.error_page = self.error_page.copy() # Put a *copy* of the class namespaces into self. self.namespaces = self.namespaces.copy() self.stage = None self.unique_id = LazyUUID4() def close(self): """Run cleanup code. (Core)""" if not self.closed: self.closed = True self.stage = 'on_end_request' self.hooks.run('on_end_request') self.stage = 'close' def run(self, method, path, query_string, req_protocol, headers, rfile): r"""Process the Request. (Core) method, path, query_string, and req_protocol should be pulled directly from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). path This should be %XX-unquoted, but query_string should not be. When using Python 2, they both MUST be byte strings, not unicode strings. When using Python 3, they both MUST be unicode strings, not byte strings, and preferably not bytes \x00-\xFF disguised as unicode. headers A list of (name, value) tuples. rfile A file-like object containing the HTTP request entity. When run() is done, the returned object should have 3 attributes: * status, e.g. "200 OK" * header_list, a list of (name, value) tuples * body, an iterable yielding strings Consumer code (HTTP servers) should then access these response attributes to build the outbound stream. """ response = cherrypy.serving.response self.stage = 'run' try: self.error_response = cherrypy.HTTPError(500).set_response self.method = method path = path or '/' self.query_string = query_string or '' self.params = {} # Compare request and server HTTP protocol versions, in case our # server does not support the requested protocol. Limit our output # to min(req, server). We want the following output: # request server actual written supported response # protocol protocol response protocol feature set # a 1.0 1.0 1.0 1.0 # b 1.0 1.1 1.1 1.0 # c 1.1 1.0 1.0 1.0 # d 1.1 1.1 1.1 1.1 # Notice that, in (b), the response will be "HTTP/1.1" even though # the client only understands 1.0. RFC 2616 10.5.6 says we should # only return 505 if the _major_ version is different. rp = int(req_protocol[5]), int(req_protocol[7]) sp = int(self.server_protocol[5]), int(self.server_protocol[7]) self.protocol = min(rp, sp) response.headers.protocol = self.protocol # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). url = path if query_string: url += '?' + query_string self.request_line = '%s %s %s' % (method, url, req_protocol) self.header_list = list(headers) self.headers = httputil.HeaderMap() self.rfile = rfile self.body = None self.cookie = SimpleCookie() self.handler = None # path_info should be the path from the # app root (script_name) to the handler. self.script_name = self.app.script_name self.path_info = pi = path[len(self.script_name):] self.stage = 'respond' self.respond(pi) except self.throws: raise except Exception: if self.throw_errors: raise else: # Failure in setup, error handler or finalize. Bypass them. # Can't use handle_error because we may not have hooks yet. cherrypy.log(traceback=True, severity=40) if self.show_tracebacks: body = format_exc() else: body = '' r = bare_error(body) response.output_status, response.header_list, response.body = r if self.method == 'HEAD': # HEAD requests MUST NOT return a message-body in the response. response.body = [] try: cherrypy.log.access() except Exception: cherrypy.log.error(traceback=True) return response def respond(self, path_info): """Generate a response for the resource at self.path_info. (Core)""" try: try: try: self._do_respond(path_info) except (cherrypy.HTTPRedirect, cherrypy.HTTPError): inst = sys.exc_info()[1] inst.set_response() self.stage = 'before_finalize (HTTPError)' self.hooks.run('before_finalize') cherrypy.serving.response.finalize() finally: self.stage = 'on_end_resource' self.hooks.run('on_end_resource') except self.throws: raise except Exception: if self.throw_errors: raise self.handle_error() def _do_respond(self, path_info): response = cherrypy.serving.response if self.app is None: raise cherrypy.NotFound() self.hooks = self.__class__.hooks.copy() self.toolmaps = {} # Get the 'Host' header, so we can HTTPRedirect properly. self.stage = 'process_headers' self.process_headers() self.stage = 'get_resource' self.get_resource(path_info) self.body = _cpreqbody.RequestBody( self.rfile, self.headers, request_params=self.params) self.namespaces(self.config) self.stage = 'on_start_resource' self.hooks.run('on_start_resource') # Parse the querystring self.stage = 'process_query_string' self.process_query_string() # Process the body if self.process_request_body: if self.method not in self.methods_with_bodies: self.process_request_body = False self.stage = 'before_request_body' self.hooks.run('before_request_body') if self.process_request_body: self.body.process() # Run the handler self.stage = 'before_handler' self.hooks.run('before_handler') if self.handler: self.stage = 'handler' response.body = self.handler() # Finalize self.stage = 'before_finalize' self.hooks.run('before_finalize') response.finalize() def process_query_string(self): """Parse the query string into Python structures. (Core)""" try: p = httputil.parse_query_string( self.query_string, encoding=self.query_string_encoding) except UnicodeDecodeError: raise cherrypy.HTTPError( 404, 'The given query string could not be processed. Query ' 'strings for this resource must be encoded with %r.' % self.query_string_encoding) self.params.update(p) def process_headers(self): """Parse HTTP header data into Python structures. (Core)""" # Process the headers into self.headers headers = self.headers for name, value in self.header_list: # Call title() now (and use dict.__method__(headers)) # so title doesn't have to be called twice. name = name.title() value = value.strip() headers[name] = httputil.decode_TEXT_maybe(value) # Some clients, notably Konquoror, supply multiple # cookies on different lines with the same key. To # handle this case, store all cookies in self.cookie. if name == 'Cookie': try: self.cookie.load(value) except CookieError as exc: raise cherrypy.HTTPError(400, str(exc)) if not dict.__contains__(headers, 'Host'): # All Internet-based HTTP/1.1 servers MUST respond with a 400 # (Bad Request) status code to any HTTP/1.1 request message # which lacks a Host header field. if self.protocol >= (1, 1): msg = "HTTP/1.1 requires a 'Host' request header." raise cherrypy.HTTPError(400, msg) else: headers['Host'] = httputil.SanitizedHost(dict.get(headers, 'Host')) host = dict.get(headers, 'Host') if not host: host = self.local.name or self.local.ip self.base = '%s://%s' % (self.scheme, host) def get_resource(self, path): """Call a dispatcher (which sets self.handler and .config). (Core)""" # First, see if there is a custom dispatch at this URI. Custom # dispatchers can only be specified in app.config, not in _cp_config # (since custom dispatchers may not even have an app.root). dispatch = self.app.find_config( path, 'request.dispatch', self.dispatch) # dispatch() should set self.handler and self.config dispatch(path) def handle_error(self): """Handle the last unanticipated exception. (Core)""" try: self.hooks.run('before_error_response') if self.error_response: self.error_response() self.hooks.run('after_error_response') cherrypy.serving.response.finalize() except cherrypy.HTTPRedirect: inst = sys.exc_info()[1] inst.set_response() cherrypy.serving.response.finalize() class ResponseBody(object): """The body of the HTTP response (the response entity).""" unicode_err = ('Page handlers MUST return bytes. Use tools.encode ' 'if you wish to return unicode.') def __get__(self, obj, objclass=None): if obj is None: # When calling on the class instead of an instance... return self else: return obj._body def __set__(self, obj, value): # Convert the given value to an iterable object. if isinstance(value, str): raise ValueError(self.unicode_err) elif isinstance(value, list): # every item in a list must be bytes... if any(isinstance(item, str) for item in value): raise ValueError(self.unicode_err) obj._body = encoding.prepare_iter(value) class Response(object): """An HTTP Response, including status, headers, and body.""" status = '' """The HTTP Status-Code and Reason-Phrase.""" header_list = [] """ A list of the HTTP response headers as (name, value) tuples. In general, you should use response.headers (a dict) instead. This attribute is generated from response.headers and is not valid until after the finalize phase.""" headers = httputil.HeaderMap() """ A dict-like object containing the response headers. Keys are header names (in Title-Case format); however, you may get and set them in a case-insensitive manner. That is, headers['Content-Type'] and headers['content-type'] refer to the same value. Values are header values (decoded according to :rfc:`2047` if necessary). .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` """ cookie = SimpleCookie() """See help(Cookie).""" body = ResponseBody() """The body (entity) of the HTTP response.""" time = None """The value of time.time() when created. Use in HTTP dates.""" stream = False """If False, buffer the response body.""" def __init__(self): self.status = None self.header_list = None self._body = [] self.time = time.time() self.headers = httputil.HeaderMap() # Since we know all our keys are titled strings, we can # bypass HeaderMap.update and get a big speed boost. dict.update(self.headers, { 'Content-Type': 'text/html', 'Server': 'CherryPy/' + cherrypy.__version__, 'Date': httputil.HTTPDate(self.time), }) self.cookie = SimpleCookie() def collapse_body(self): """Collapse self.body to a single string; replace it and return it.""" new_body = b''.join(self.body) self.body = new_body return new_body def _flush_body(self): """ Discard self.body but consume any generator such that any finalization can occur, such as is required by caching.tee_output(). """ consume(iter(self.body)) def finalize(self): """Transform headers (and cookies) into self.header_list. (Core)""" try: code, reason, _ = httputil.valid_status(self.status) except ValueError: raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) headers = self.headers self.status = '%s %s' % (code, reason) self.output_status = ntob(str(code), 'ascii') + \ b' ' + headers.encode(reason) if self.stream: # The upshot: wsgiserver will chunk the response if # you pop Content-Length (or set it explicitly to None). # Note that lib.static sets C-L to the file's st_size. if dict.get(headers, 'Content-Length') is None: dict.pop(headers, 'Content-Length', None) elif code < 200 or code in (204, 205, 304): # "All 1xx (informational), 204 (no content), # and 304 (not modified) responses MUST NOT # include a message-body." dict.pop(headers, 'Content-Length', None) self._flush_body() self.body = b'' else: # Responses which are not streamed should have a Content-Length, # but allow user code to set Content-Length if desired. if dict.get(headers, 'Content-Length') is None: content = self.collapse_body() dict.__setitem__(headers, 'Content-Length', len(content)) # Transform our header dict into a list of tuples. self.header_list = h = headers.output() cookie = self.cookie.output() if cookie: for line in cookie.split('\r\n'): name, value = line.split(': ', 1) if isinstance(name, str): name = name.encode('ISO-8859-1') if isinstance(value, str): value = headers.encode(value) h.append((name, value)) class LazyUUID4(object): def __str__(self): """Return UUID4 and keep it for future calls.""" return str(self.uuid4) @property def uuid4(self): """Provide unique id on per-request basis using UUID4. It's evaluated lazily on render. """ try: self._uuid4 except AttributeError: # evaluate on first access self._uuid4 = uuid.uuid4() return self._uuid4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpserver.py0000644252176402575230000002020014307416152020434 0ustar00jaracoprimarygroup"""Manage HTTP servers with CherryPy.""" import cherrypy from cherrypy.lib.reprconf import attributes from cherrypy._cpcompat import text_or_bytes from cherrypy.process.servers import ServerAdapter __all__ = ('Server', ) class Server(ServerAdapter): """An adapter for an HTTP server. You can set attributes (like socket_host and socket_port) on *this* object (which is probably cherrypy.server), and call quickstart. For example:: cherrypy.server.socket_port = 80 cherrypy.quickstart() """ socket_port = 8080 """The TCP port on which to listen for connections.""" _socket_host = '127.0.0.1' @property def socket_host(self): # noqa: D401; irrelevant for properties """The hostname or IP address on which to listen for connections. Host values may be any IPv4 or IPv6 address, or any valid hostname. The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). The string '0.0.0.0' is a special IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for IPv6. The empty string or None are not allowed. """ return self._socket_host @socket_host.setter def socket_host(self, value): if value == '': raise ValueError("The empty string ('') is not an allowed value. " "Use '0.0.0.0' instead to listen on all active " 'interfaces (INADDR_ANY).') self._socket_host = value socket_file = None """If given, the name of the UNIX socket to use instead of TCP/IP. When this option is not None, the `socket_host` and `socket_port` options are ignored.""" socket_queue_size = 5 """The 'backlog' argument to socket.listen(); specifies the maximum number of queued connections (default 5).""" socket_timeout = 10 """The timeout in seconds for accepted connections (default 10).""" accepted_queue_size = -1 """The maximum number of requests which will be queued up before the server refuses to accept it (default -1, meaning no limit).""" accepted_queue_timeout = 10 """The timeout in seconds for attempting to add a request to the queue when the queue is full (default 10).""" shutdown_timeout = 5 """The time to wait for HTTP worker threads to clean up.""" protocol_version = 'HTTP/1.1' """The version string to write in the Status-Line of all HTTP responses, for example, "HTTP/1.1" (the default). Depending on the HTTP server used, this should also limit the supported features used in the response.""" thread_pool = 10 """The number of worker threads to start up in the pool.""" thread_pool_max = -1 """The maximum size of the worker-thread pool. Use -1 to indicate no limit. """ max_request_header_size = 500 * 1024 """The maximum number of bytes allowable in the request headers. If exceeded, the HTTP server should return "413 Request Entity Too Large". """ max_request_body_size = 100 * 1024 * 1024 """The maximum number of bytes allowable in the request body. If exceeded, the HTTP server should return "413 Request Entity Too Large".""" instance = None """If not None, this should be an HTTP server instance (such as cheroot.wsgi.Server) which cherrypy.server will control. Use this when you need more control over object instantiation than is available in the various configuration options.""" ssl_context = None """When using PyOpenSSL, an instance of SSL.Context.""" ssl_certificate = None """The filename of the SSL certificate to use.""" ssl_certificate_chain = None """When using PyOpenSSL, the certificate chain to pass to Context.load_verify_locations.""" ssl_private_key = None """The filename of the private key to use with SSL.""" ssl_ciphers = None """The ciphers list of SSL.""" ssl_module = 'builtin' """The name of a registered SSL adaptation module to use with the builtin WSGI server. Builtin options are: 'builtin' (to use the SSL library built into recent versions of Python). You may also register your own classes in the cheroot.server.ssl_adapters dict.""" statistics = False """Turns statistics-gathering on or off for aware HTTP servers.""" nodelay = True """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" wsgi_version = (1, 0) """The WSGI version tuple to use with the builtin WSGI server. The provided options are (1, 0) [which includes support for PEP 3333, which declares it covers WSGI version 1.0.1 but still mandates the wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. You may create and register your own experimental versions of the WSGI protocol by adding custom classes to the cheroot.server.wsgi_gateways dict. """ peercreds = False """If True, peer cred lookup for UNIX domain socket will put to WSGI env. This information will then be available through WSGI env vars: * X_REMOTE_PID * X_REMOTE_UID * X_REMOTE_GID """ peercreds_resolve = False """If True, username/group will be looked up in the OS from peercreds. This information will then be available through WSGI env vars: * REMOTE_USER * X_REMOTE_USER * X_REMOTE_GROUP """ def __init__(self): """Initialize Server instance.""" self.bus = cherrypy.engine self.httpserver = None self.interrupt = None self.running = False def httpserver_from_self(self, httpserver=None): """Return a (httpserver, bind_addr) pair based on self attributes.""" if httpserver is None: httpserver = self.instance if httpserver is None: from cherrypy import _cpwsgi_server httpserver = _cpwsgi_server.CPWSGIServer(self) if isinstance(httpserver, text_or_bytes): # Is anyone using this? Can I add an arg? httpserver = attributes(httpserver)(self) return httpserver, self.bind_addr def start(self): """Start the HTTP server.""" if not self.httpserver: self.httpserver, self.bind_addr = self.httpserver_from_self() super(Server, self).start() start.priority = 75 @property def bind_addr(self): """Return bind address. A (host, port) tuple for TCP sockets or a str for Unix domain sockts. """ if self.socket_file: return self.socket_file if self.socket_host is None and self.socket_port is None: return None return (self.socket_host, self.socket_port) @bind_addr.setter def bind_addr(self, value): if value is None: self.socket_file = None self.socket_host = None self.socket_port = None elif isinstance(value, text_or_bytes): self.socket_file = value self.socket_host = None self.socket_port = None else: try: self.socket_host, self.socket_port = value self.socket_file = None except ValueError: raise ValueError('bind_addr must be a (host, port) tuple ' '(for TCP sockets) or a string (for Unix ' 'domain sockets), not %r' % value) def base(self): """Return the base for this server. e.i. scheme://host[:port] or sock file """ if self.socket_file: return self.socket_file host = self.socket_host if host in ('0.0.0.0', '::'): # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. # Look up the host name, which should be the # safest thing to spit out in a URL. import socket host = socket.gethostname() port = self.socket_port if self.ssl_certificate: scheme = 'https' if port != 443: host += ':%s' % port else: scheme = 'http' if port != 80: host += ':%s' % port return '%s://%s' % (scheme, host) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cptools.py0000644252176402575230000004336314307416152020305 0ustar00jaracoprimarygroup"""CherryPy tools. A "tool" is any helper, adapted to CP. Tools are usually designed to be used in a variety of ways (although some may only offer one if they choose): Library calls All tools are callables that can be used wherever needed. The arguments are straightforward and should be detailed within the docstring. Function decorators All tools, when called, may be used as decorators which configure individual CherryPy page handlers (methods on the CherryPy tree). That is, "@tools.anytool()" should "turn on" the tool via the decorated function's _cp_config attribute. CherryPy config If a tool exposes a "_setup" callable, it will be called once per Request (if the feature is "turned on" via config). Tools may be implemented as any object with a namespace. The builtins are generally either modules or instances of the tools.Tool class. """ import cherrypy from cherrypy._helper import expose from cherrypy.lib import cptools, encoding, static, jsontools from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc from cherrypy.lib import caching as _caching from cherrypy.lib import auth_basic, auth_digest def _getargs(func): """Return the names of all static arguments to the given function.""" # Use this instead of importing inspect for less mem overhead. import types if isinstance(func, types.MethodType): func = func.__func__ co = func.__code__ return co.co_varnames[:co.co_argcount] _attr_error = ( 'CherryPy Tools cannot be turned on directly. Instead, turn them ' 'on via config, or use them as decorators on your page handlers.' ) class Tool(object): """A registered function for use with CherryPy request-processing hooks. help(tool.callable) should give you more information about this Tool. """ namespace = 'tools' def __init__(self, point, callable, name=None, priority=50): self._point = point self.callable = callable self._name = name self._priority = priority self.__doc__ = self.callable.__doc__ self._setargs() @property def on(self): raise AttributeError(_attr_error) @on.setter def on(self, value): raise AttributeError(_attr_error) def _setargs(self): """Copy func parameter names to obj attributes.""" try: for arg in _getargs(self.callable): setattr(self, arg, None) except (TypeError, AttributeError): if hasattr(self.callable, '__call__'): for arg in _getargs(self.callable.__call__): setattr(self, arg, None) # IronPython 1.0 raises NotImplementedError because # inspect.getargspec tries to access Python bytecode # in co_code attribute. except NotImplementedError: pass # IronPython 1B1 may raise IndexError in some cases, # but if we trap it here it doesn't prevent CP from working. except IndexError: pass def _merged_args(self, d=None): """Return a dict of configuration entries for this Tool.""" if d: conf = d.copy() else: conf = {} tm = cherrypy.serving.request.toolmaps[self.namespace] if self._name in tm: conf.update(tm[self._name]) if 'on' in conf: del conf['on'] return conf def __call__(self, *args, **kwargs): """Compile-time decorator (turn on the tool in config). For example:: @expose @tools.proxy() def whats_my_base(self): return cherrypy.request.base """ if args: raise TypeError('The %r Tool does not accept positional ' 'arguments; you must use keyword arguments.' % self._name) def tool_decorator(f): if not hasattr(f, '_cp_config'): f._cp_config = {} subspace = self.namespace + '.' + self._name + '.' f._cp_config[subspace + 'on'] = True for k, v in kwargs.items(): f._cp_config[subspace + k] = v return f return tool_decorator def _setup(self): """Hook this tool into cherrypy.request. The standard CherryPy request object will automatically call this method when the tool is "turned on" in config. """ conf = self._merged_args() p = conf.pop('priority', None) if p is None: p = getattr(self.callable, 'priority', self._priority) cherrypy.serving.request.hooks.attach(self._point, self.callable, priority=p, **conf) class HandlerTool(Tool): """Tool which is called 'before main', that may skip normal handlers. If the tool successfully handles the request (by setting response.body), if should return True. This will cause CherryPy to skip any 'normal' page handler. If the tool did not handle the request, it should return False to tell CherryPy to continue on and call the normal page handler. If the tool is declared AS a page handler (see the 'handler' method), returning False will raise NotFound. """ def __init__(self, callable, name=None): Tool.__init__(self, 'before_handler', callable, name) def handler(self, *args, **kwargs): """Use this tool as a CherryPy page handler. For example:: class Root: nav = tools.staticdir.handler(section="/nav", dir="nav", root=absDir) """ @expose def handle_func(*a, **kw): handled = self.callable(*args, **self._merged_args(kwargs)) if not handled: raise cherrypy.NotFound() return cherrypy.serving.response.body return handle_func def _wrapper(self, **kwargs): if self.callable(**kwargs): cherrypy.serving.request.handler = None def _setup(self): """Hook this tool into cherrypy.request. The standard CherryPy request object will automatically call this method when the tool is "turned on" in config. """ conf = self._merged_args() p = conf.pop('priority', None) if p is None: p = getattr(self.callable, 'priority', self._priority) cherrypy.serving.request.hooks.attach(self._point, self._wrapper, priority=p, **conf) class HandlerWrapperTool(Tool): """Tool which wraps request.handler in a provided wrapper function. The 'newhandler' arg must be a handler wrapper function that takes a 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all page handler functions, it must return an iterable for use as cherrypy.response.body. For example, to allow your 'inner' page handlers to return dicts which then get interpolated into a template:: def interpolator(next_handler, *args, **kwargs): filename = cherrypy.request.config.get('template') cherrypy.response.template = env.get_template(filename) response_dict = next_handler(*args, **kwargs) return cherrypy.response.template.render(**response_dict) cherrypy.tools.jinja = HandlerWrapperTool(interpolator) """ def __init__(self, newhandler, point='before_handler', name=None, priority=50): self.newhandler = newhandler self._point = point self._name = name self._priority = priority def callable(self, *args, **kwargs): innerfunc = cherrypy.serving.request.handler def wrap(*args, **kwargs): return self.newhandler(innerfunc, *args, **kwargs) cherrypy.serving.request.handler = wrap class ErrorTool(Tool): """Tool which is used to replace the default request.error_response.""" def __init__(self, callable, name=None): Tool.__init__(self, None, callable, name) def _wrapper(self): self.callable(**self._merged_args()) def _setup(self): """Hook this tool into cherrypy.request. The standard CherryPy request object will automatically call this method when the tool is "turned on" in config. """ cherrypy.serving.request.error_response = self._wrapper # Builtin tools # class SessionTool(Tool): """Session Tool for CherryPy. sessions.locking When 'implicit' (the default), the session will be locked for you, just before running the page handler. When 'early', the session will be locked before reading the request body. This is off by default for safety reasons; for example, a large upload would block the session, denying an AJAX progress meter (`issue `_). When 'explicit' (or any other value), you need to call cherrypy.session.acquire_lock() yourself before using session data. """ def __init__(self): # _sessions.init must be bound after headers are read Tool.__init__(self, 'before_request_body', _sessions.init) def _lock_session(self): cherrypy.serving.session.acquire_lock() def _setup(self): """Hook this tool into cherrypy.request. The standard CherryPy request object will automatically call this method when the tool is "turned on" in config. """ hooks = cherrypy.serving.request.hooks conf = self._merged_args() p = conf.pop('priority', None) if p is None: p = getattr(self.callable, 'priority', self._priority) hooks.attach(self._point, self.callable, priority=p, **conf) locking = conf.pop('locking', 'implicit') if locking == 'implicit': hooks.attach('before_handler', self._lock_session) elif locking == 'early': # Lock before the request body (but after _sessions.init runs!) hooks.attach('before_request_body', self._lock_session, priority=60) else: # Don't lock pass hooks.attach('before_finalize', _sessions.save) hooks.attach('on_end_request', _sessions.close) def regenerate(self): """Drop the current session and make a new one (with a new id).""" sess = cherrypy.serving.session sess.regenerate() # Grab cookie-relevant tool args relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure' conf = dict( (k, v) for k, v in self._merged_args().items() if k in relevant ) _sessions.set_response_cookie(**conf) class XMLRPCController(object): """A Controller (page handler collection) for XML-RPC. To use it, have your controllers subclass this base class (it will turn on the tool for you). You can also supply the following optional config entries:: tools.xmlrpc.encoding: 'utf-8' tools.xmlrpc.allow_none: 0 XML-RPC is a rather discontinuous layer over HTTP; dispatching to the appropriate handler must first be performed according to the URL, and then a second dispatch step must take place according to the RPC method specified in the request body. It also allows a superfluous "/RPC2" prefix in the URL, supplies its own handler args in the body, and requires a 200 OK "Fault" response instead of 404 when the desired method is not found. Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. This Controller acts as the dispatch target for the first half (based on the URL); it then reads the RPC method from the request body and does its own second dispatch step based on that method. It also reads body params, and returns a Fault on error. The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 in your URL's, you can safely skip turning on the XMLRPCDispatcher. Otherwise, you need to use declare it in config:: request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() """ # Note we're hard-coding this into the 'tools' namespace. We could do # a huge amount of work to make it relocatable, but the only reason why # would be if someone actually disabled the default_toolbox. Meh. _cp_config = {'tools.xmlrpc.on': True} @expose def default(self, *vpath, **params): rpcparams, rpcmethod = _xmlrpc.process_body() subhandler = self for attr in str(rpcmethod).split('.'): subhandler = getattr(subhandler, attr, None) if subhandler and getattr(subhandler, 'exposed', False): body = subhandler(*(vpath + rpcparams), **params) else: # https://github.com/cherrypy/cherrypy/issues/533 # if a method is not found, an xmlrpclib.Fault should be returned # raising an exception here will do that; see # cherrypy.lib.xmlrpcutil.on_error raise Exception('method "%s" is not supported' % attr) conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {}) _xmlrpc.respond(body, conf.get('encoding', 'utf-8'), conf.get('allow_none', 0)) return cherrypy.serving.response.body class SessionAuthTool(HandlerTool): pass class CachingTool(Tool): """Caching Tool for CherryPy.""" def _wrapper(self, **kwargs): request = cherrypy.serving.request if _caching.get(**kwargs): request.handler = None else: if request.cacheable: # Note the devious technique here of adding hooks on the fly request.hooks.attach('before_finalize', _caching.tee_output, priority=100) _wrapper.priority = 90 def _setup(self): """Hook caching into cherrypy.request.""" conf = self._merged_args() p = conf.pop('priority', None) cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, priority=p, **conf) class Toolbox(object): """A collection of Tools. This object also functions as a config namespace handler for itself. Custom toolboxes should be added to each Application's toolboxes dict. """ def __init__(self, namespace): self.namespace = namespace def __setattr__(self, name, value): # If the Tool._name is None, supply it from the attribute name. if isinstance(value, Tool): if value._name is None: value._name = name value.namespace = self.namespace object.__setattr__(self, name, value) def __enter__(self): """Populate request.toolmaps from tools specified in config.""" cherrypy.serving.request.toolmaps[self.namespace] = map = {} def populate(k, v): toolname, arg = k.split('.', 1) bucket = map.setdefault(toolname, {}) bucket[arg] = v return populate def __exit__(self, exc_type, exc_val, exc_tb): """Run tool._setup() for each tool in our toolmap.""" map = cherrypy.serving.request.toolmaps.get(self.namespace) if map: for name, settings in map.items(): if settings.get('on', False): tool = getattr(self, name) tool._setup() def register(self, point, **kwargs): """ Return a decorator which registers the function at the given hook point. """ def decorator(func): attr_name = kwargs.get('name', func.__name__) tool = Tool(point, func, **kwargs) setattr(self, attr_name, tool) return func return decorator default_toolbox = _d = Toolbox('tools') _d.session_auth = SessionAuthTool(cptools.session_auth) _d.allow = Tool('on_start_resource', cptools.allow) _d.proxy = Tool('before_request_body', cptools.proxy, priority=30) _d.response_headers = Tool('on_start_resource', cptools.response_headers) _d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) _d.log_headers = Tool('before_error_response', cptools.log_request_headers) _d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) _d.err_redirect = ErrorTool(cptools.redirect) _d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) _d.decode = Tool('before_request_body', encoding.decode) # the order of encoding, gzip, caching is important _d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) _d.gzip = Tool('before_finalize', encoding.gzip, priority=80) _d.staticdir = HandlerTool(static.staticdir) _d.staticfile = HandlerTool(static.staticfile) _d.sessions = SessionTool() _d.xmlrpc = ErrorTool(_xmlrpc.on_error) _d.caching = CachingTool('before_handler', _caching.get, 'caching') _d.expires = Tool('before_finalize', _caching.expires) _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) _d.referer = Tool('before_request_body', cptools.referer) _d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) _d.flatten = Tool('before_finalize', cptools.flatten) _d.accept = Tool('on_start_resource', cptools.accept) _d.redirect = Tool('on_start_resource', cptools.redirect) _d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) _d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) _d.json_out = Tool('before_handler', jsontools.json_out, priority=30) _d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) _d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) _d.params = Tool('before_handler', cptools.convert_params, priority=15) del _d, cptools, encoding, static ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cptree.py0000644252176402575230000002534114307416152020100 0ustar00jaracoprimarygroup"""CherryPy Application and Tree objects.""" import os import cherrypy from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools from cherrypy.lib import httputil, reprconf class Application(object): """A CherryPy Application. Servers and gateways should not instantiate Request objects directly. Instead, they should ask an Application object for a request object. An instance of this class may also be used as a WSGI callable (WSGI application object) for itself. """ root = None """The top-most container of page handlers for this app. Handlers should be arranged in a hierarchy of attributes, matching the expected URI hierarchy; the default dispatcher then searches this hierarchy for a matching handler. When using a dispatcher other than the default, this value may be None.""" config = {} """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict of {key: value} pairs.""" namespaces = reprconf.NamespaceSet() toolboxes = {'tools': cherrypy.tools} log = None """A LogManager instance. See _cplogging.""" wsgiapp = None """A CPWSGIApp instance. See _cpwsgi.""" request_class = _cprequest.Request response_class = _cprequest.Response relative_urls = False def __init__(self, root, script_name='', config=None): """Initialize Application with given root.""" self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) self.root = root self.script_name = script_name self.wsgiapp = _cpwsgi.CPWSGIApp(self) self.namespaces = self.namespaces.copy() self.namespaces['log'] = lambda k, v: setattr(self.log, k, v) self.namespaces['wsgi'] = self.wsgiapp.namespace_handler self.config = self.__class__.config.copy() if config: self.merge(config) def __repr__(self): """Generate a representation of the Application instance.""" return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__, self.root, self.script_name) script_name_doc = """The URI "mount point" for this app. A mount point is that portion of the URI which is constant for all URIs that are serviced by this application; it does not include scheme, host, or proxy ("virtual host") portions of the URI. For example, if script_name is "/my/cool/app", then the URL "http://www.example.com/my/cool/app/page1" might be handled by a "page1" method on the root object. The value of script_name MUST NOT end in a slash. If the script_name refers to the root of the URI, it MUST be an empty string (not "/"). If script_name is explicitly set to None, then the script_name will be provided for each call from request.wsgi_environ['SCRIPT_NAME']. """ @property def script_name(self): # noqa: D401; irrelevant for properties """The URI "mount point" for this app. A mount point is that portion of the URI which is constant for all URIs that are serviced by this application; it does not include scheme, host, or proxy ("virtual host") portions of the URI. For example, if script_name is "/my/cool/app", then the URL "http://www.example.com/my/cool/app/page1" might be handled by a "page1" method on the root object. The value of script_name MUST NOT end in a slash. If the script_name refers to the root of the URI, it MUST be an empty string (not "/"). If script_name is explicitly set to None, then the script_name will be provided for each call from request.wsgi_environ['SCRIPT_NAME']. """ if self._script_name is not None: return self._script_name # A `_script_name` with a value of None signals that the script name # should be pulled from WSGI environ. return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/') @script_name.setter def script_name(self, value): if value: value = value.rstrip('/') self._script_name = value def merge(self, config): """Merge the given config into self.config.""" _cpconfig.merge(self.config, config) # Handle namespaces specified in config. self.namespaces(self.config.get('/', {})) def find_config(self, path, key, default=None): """Return the most-specific value for key along path, or default.""" trail = path or '/' while trail: nodeconf = self.config.get(trail, {}) if key in nodeconf: return nodeconf[key] lastslash = trail.rfind('/') if lastslash == -1: break elif lastslash == 0 and trail != '/': trail = '/' else: trail = trail[:lastslash] return default def get_serving(self, local, remote, scheme, sproto): """Create and return a Request and Response object.""" req = self.request_class(local, remote, scheme, sproto) req.app = self for name, toolbox in self.toolboxes.items(): req.namespaces[name] = toolbox resp = self.response_class() cherrypy.serving.load(req, resp) cherrypy.engine.publish('acquire_thread') cherrypy.engine.publish('before_request') return req, resp def release_serving(self): """Release the current serving (request and response).""" req = cherrypy.serving.request cherrypy.engine.publish('after_request') try: req.close() except Exception: cherrypy.log(traceback=True, severity=40) cherrypy.serving.clear() def __call__(self, environ, start_response): """Call a WSGI-callable.""" return self.wsgiapp(environ, start_response) class Tree(object): """A registry of CherryPy applications, mounted at diverse points. An instance of this class may also be used as a WSGI callable (WSGI application object), in which case it dispatches to all mounted apps. """ apps = {} """ A dict of the form {script name: application}, where "script name" is a string declaring the URI mount point (no trailing slash), and "application" is an instance of cherrypy.Application (or an arbitrary WSGI callable if you happen to be using a WSGI server).""" def __init__(self): """Initialize registry Tree.""" self.apps = {} def mount(self, root, script_name='', config=None): """Mount a new app from a root object, script_name, and config. root An instance of a "controller class" (a collection of page handler methods) which represents the root of the application. This may also be an Application instance, or None if using a dispatcher other than the default. script_name A string containing the "mount point" of the application. This should start with a slash, and be the path portion of the URL at which to mount the given root. For example, if root.index() will handle requests to "http://www.example.com:8080/dept/app1/", then the script_name argument would be "/dept/app1". It MUST NOT end in a slash. If the script_name refers to the root of the URI, it MUST be an empty string (not "/"). config A file or dict containing application config. """ if script_name is None: raise TypeError( "The 'script_name' argument may not be None. Application " 'objects may, however, possess a script_name of None (in ' 'order to inpect the WSGI environ for SCRIPT_NAME upon each ' 'request). You cannot mount such Applications on this Tree; ' 'you must pass them to a WSGI server interface directly.') # Next line both 1) strips trailing slash and 2) maps "/" -> "". script_name = script_name.rstrip('/') if isinstance(root, Application): app = root if script_name != '' and script_name != app.script_name: raise ValueError( 'Cannot specify a different script name and pass an ' 'Application instance to cherrypy.mount') script_name = app.script_name else: app = Application(root, script_name) # If mounted at "", add favicon.ico needs_favicon = ( script_name == '' and root is not None and not hasattr(root, 'favicon_ico') ) if needs_favicon: favicon = os.path.join( os.getcwd(), os.path.dirname(__file__), 'favicon.ico', ) root.favicon_ico = tools.staticfile.handler(favicon) if config: app.merge(config) self.apps[script_name] = app return app def graft(self, wsgi_callable, script_name=''): """Mount a wsgi callable at the given script_name.""" # Next line both 1) strips trailing slash and 2) maps "/" -> "". script_name = script_name.rstrip('/') self.apps[script_name] = wsgi_callable def script_name(self, path=None): """Return the script_name of the app at the given path, or None. If path is None, cherrypy.request is used. """ if path is None: try: request = cherrypy.serving.request path = httputil.urljoin(request.script_name, request.path_info) except AttributeError: return None while True: if path in self.apps: return path if path == '': return None # Move one node up the tree and try again. path = path[:path.rfind('/')] def __call__(self, environ, start_response): """Pre-initialize WSGI env and call WSGI-callable.""" # If you're calling this, then you're probably setting SCRIPT_NAME # to '' (some WSGI servers always set SCRIPT_NAME to ''). # Try to look up the app using the full path. env1x = environ path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), env1x.get('PATH_INFO', '')) sn = self.script_name(path or '/') if sn is None: start_response('404 Not Found', []) return [] app = self.apps[sn] # Correct the SCRIPT_NAME and PATH_INFO environ entries. environ = environ.copy() environ['SCRIPT_NAME'] = sn environ['PATH_INFO'] = path[len(sn.rstrip('/')):] return app(environ, start_response) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpwsgi.py0000644252176402575230000004001214307416152020102 0ustar00jaracoprimarygroup"""WSGI interface (see PEP 333 and 3333). Note that WSGI environ keys and values are 'native strings'; that is, whatever the type of "" is. For Python 2, that's a byte string; for Python 3, it's a unicode string. But PEP 3333 says: "even if Python's str type is actually Unicode "under the hood", the content of native strings must still be translatable to bytes via the Latin-1 encoding!" """ import sys as _sys import io import cherrypy as _cherrypy from cherrypy._cpcompat import ntou from cherrypy import _cperror from cherrypy.lib import httputil from cherrypy.lib import is_closable_iterator def downgrade_wsgi_ux_to_1x(environ): """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ. """ env1x = {} url_encoding = environ[ntou('wsgi.url_encoding')] for k, v in environ.copy().items(): if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: v = v.encode(url_encoding) elif isinstance(v, str): v = v.encode('ISO-8859-1') env1x[k.encode('ISO-8859-1')] = v return env1x class VirtualHost(object): """Select a different WSGI application based on the Host header. This can be useful when running multiple sites within one CP server. It allows several domains to point to different applications. For example:: root = Root() RootApp = cherrypy.Application(root) Domain2App = cherrypy.Application(root) SecureApp = cherrypy.Application(Secure()) vhost = cherrypy._cpwsgi.VirtualHost( RootApp, domains={ 'www.domain2.example': Domain2App, 'www.domain2.example:443': SecureApp, }, ) cherrypy.tree.graft(vhost) """ default = None """Required. The default WSGI application.""" use_x_forwarded_host = True """If True (the default), any "X-Forwarded-Host" request header will be used instead of the "Host" header. This is commonly added by HTTP servers (such as Apache) when proxying.""" domains = {} """A dict of {host header value: application} pairs. The incoming "Host" request header is looked up in this dict, and, if a match is found, the corresponding WSGI application will be called instead of the default. Note that you often need separate entries for "example.com" and "www.example.com". In addition, "Host" headers may contain the port number. """ def __init__(self, default, domains=None, use_x_forwarded_host=True): self.default = default self.domains = domains or {} self.use_x_forwarded_host = use_x_forwarded_host def __call__(self, environ, start_response): domain = environ.get('HTTP_HOST', '') if self.use_x_forwarded_host: domain = environ.get('HTTP_X_FORWARDED_HOST', domain) nextapp = self.domains.get(domain) if nextapp is None: nextapp = self.default return nextapp(environ, start_response) class InternalRedirector(object): """WSGI middleware that handles raised cherrypy.InternalRedirect.""" def __init__(self, nextapp, recursive=False): self.nextapp = nextapp self.recursive = recursive def __call__(self, environ, start_response): redirections = [] while True: environ = environ.copy() try: return self.nextapp(environ, start_response) except _cherrypy.InternalRedirect: ir = _sys.exc_info()[1] sn = environ.get('SCRIPT_NAME', '') path = environ.get('PATH_INFO', '') qs = environ.get('QUERY_STRING', '') # Add the *previous* path_info + qs to redirections. old_uri = sn + path if qs: old_uri += '?' + qs redirections.append(old_uri) if not self.recursive: # Check to see if the new URI has been redirected to # already new_uri = sn + ir.path if ir.query_string: new_uri += '?' + ir.query_string if new_uri in redirections: ir.request.close() tmpl = ( 'InternalRedirector visited the same URL twice: %r' ) raise RuntimeError(tmpl % new_uri) # Munge the environment and try again. environ['REQUEST_METHOD'] = 'GET' environ['PATH_INFO'] = ir.path environ['QUERY_STRING'] = ir.query_string environ['wsgi.input'] = io.BytesIO() environ['CONTENT_LENGTH'] = '0' environ['cherrypy.previous_request'] = ir.request class ExceptionTrapper(object): """WSGI middleware that traps exceptions.""" def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): self.nextapp = nextapp self.throws = throws def __call__(self, environ, start_response): return _TrappedResponse( self.nextapp, environ, start_response, self.throws ) class _TrappedResponse(object): response = iter([]) def __init__(self, nextapp, environ, start_response, throws): self.nextapp = nextapp self.environ = environ self.start_response = start_response self.throws = throws self.started_response = False self.response = self.trap( self.nextapp, self.environ, self.start_response, ) self.iter_response = iter(self.response) def __iter__(self): self.started_response = True return self def __next__(self): return self.trap(next, self.iter_response) def close(self): if hasattr(self.response, 'close'): self.response.close() def trap(self, func, *args, **kwargs): try: return func(*args, **kwargs) except self.throws: raise except StopIteration: raise except Exception: tb = _cperror.format_exc() _cherrypy.log(tb, severity=40) if not _cherrypy.request.show_tracebacks: tb = '' s, h, b = _cperror.bare_error(tb) if True: # What fun. s = s.decode('ISO-8859-1') h = [ (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) for k, v in h ] if self.started_response: # Empty our iterable (so future calls raise StopIteration) self.iter_response = iter([]) else: self.iter_response = iter(b) try: self.start_response(s, h, _sys.exc_info()) except Exception: # "The application must not trap any exceptions raised by # start_response, if it called start_response with exc_info. # Instead, it should allow such exceptions to propagate # back to the server or gateway." # But we still log and call close() to clean up ourselves. _cherrypy.log(traceback=True, severity=40) raise if self.started_response: return b''.join(b) else: return b # WSGI-to-CP Adapter # class AppResponse(object): """WSGI response iterable for CherryPy applications.""" def __init__(self, environ, start_response, cpapp): self.cpapp = cpapp try: self.environ = environ self.run() r = _cherrypy.serving.response outstatus = r.output_status if not isinstance(outstatus, bytes): raise TypeError('response.output_status is not a byte string.') outheaders = [] for k, v in r.header_list: if not isinstance(k, bytes): tmpl = 'response.header_list key %r is not a byte string.' raise TypeError(tmpl % k) if not isinstance(v, bytes): tmpl = ( 'response.header_list value %r is not a byte string.' ) raise TypeError(tmpl % v) outheaders.append((k, v)) if True: # According to PEP 3333, when using Python 3, the response # status and headers must be bytes masquerading as unicode; # that is, they must be of type "str" but are restricted to # code points in the "latin-1" set. outstatus = outstatus.decode('ISO-8859-1') outheaders = [ (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) for k, v in outheaders ] self.iter_response = iter(r.body) self.write = start_response(outstatus, outheaders) except BaseException: self.close() raise def __iter__(self): return self def __next__(self): return next(self.iter_response) def close(self): """Close and de-reference the current request and response. (Core)""" streaming = _cherrypy.serving.response.stream self.cpapp.release_serving() # We avoid the expense of examining the iterator to see if it's # closable unless we are streaming the response, as that's the # only situation where we are going to have an iterator which # may not have been exhausted yet. if streaming and is_closable_iterator(self.iter_response): iter_close = self.iter_response.close try: iter_close() except Exception: _cherrypy.log(traceback=True, severity=40) def run(self): """Create a Request object using environ.""" env = self.environ.get local = httputil.Host( '', int(env('SERVER_PORT', 80) or -1), env('SERVER_NAME', ''), ) remote = httputil.Host( env('REMOTE_ADDR', ''), int(env('REMOTE_PORT', -1) or -1), env('REMOTE_HOST', ''), ) scheme = env('wsgi.url_scheme') sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1') request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) # LOGON_USER is served by IIS, and is the name of the # user after having been mapped to a local account. # Both IIS and Apache set REMOTE_USER, when possible. request.login = env('LOGON_USER') or env('REMOTE_USER') or None request.multithread = self.environ['wsgi.multithread'] request.multiprocess = self.environ['wsgi.multiprocess'] request.wsgi_environ = self.environ request.prev = env('cherrypy.previous_request', None) meth = self.environ['REQUEST_METHOD'] path = httputil.urljoin( self.environ.get('SCRIPT_NAME', ''), self.environ.get('PATH_INFO', ''), ) qs = self.environ.get('QUERY_STRING', '') path, qs = self.recode_path_qs(path, qs) or (path, qs) rproto = self.environ.get('SERVER_PROTOCOL') headers = self.translate_headers(self.environ) rfile = self.environ['wsgi.input'] request.run(meth, path, qs, rproto, headers, rfile) headerNames = { 'HTTP_CGI_AUTHORIZATION': 'Authorization', 'CONTENT_LENGTH': 'Content-Length', 'CONTENT_TYPE': 'Content-Type', 'REMOTE_HOST': 'Remote-Host', 'REMOTE_ADDR': 'Remote-Addr', } def recode_path_qs(self, path, qs): # This isn't perfect; if the given PATH_INFO is in the # wrong encoding, it may fail to match the appropriate config # section URI. But meh. old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') new_enc = self.cpapp.find_config( self.environ.get('PATH_INFO', ''), 'request.uri_encoding', 'utf-8', ) if new_enc.lower() == old_enc.lower(): return # Even though the path and qs are unicode, the WSGI server # is required by PEP 3333 to coerce them to ISO-8859-1 # masquerading as unicode. So we have to encode back to # bytes and then decode again using the "correct" encoding. try: return ( path.encode(old_enc).decode(new_enc), qs.encode(old_enc).decode(new_enc), ) except (UnicodeEncodeError, UnicodeDecodeError): # Just pass them through without transcoding and hope. pass def translate_headers(self, environ): """Translate CGI-environ header names to HTTP header names.""" for cgiName in environ: # We assume all incoming header keys are uppercase already. if cgiName in self.headerNames: yield self.headerNames[cgiName], environ[cgiName] elif cgiName[:5] == 'HTTP_': # Hackish attempt at recovering original header names. translatedHeader = cgiName[5:].replace('_', '-') yield translatedHeader, environ[cgiName] class CPWSGIApp(object): """A WSGI application object for a CherryPy Application.""" pipeline = [ ('ExceptionTrapper', ExceptionTrapper), ('InternalRedirector', InternalRedirector), ] """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a constructor that takes an initial, positional 'nextapp' argument, plus optional keyword arguments, and returns a WSGI application (that takes environ and start_response arguments). The 'name' can be any you choose, and will correspond to keys in self.config.""" head = None """Rather than nest all apps in the pipeline on each call, it's only done the first time, and the result is memoized into self.head. Set this to None again if you change self.pipeline after calling self.""" config = {} """A dict whose keys match names listed in the pipeline. Each value is a further dict which will be passed to the corresponding named WSGI callable (from the pipeline) as keyword arguments.""" response_class = AppResponse """The class to instantiate and return as the next app in the WSGI chain. """ def __init__(self, cpapp, pipeline=None): self.cpapp = cpapp self.pipeline = self.pipeline[:] if pipeline: self.pipeline.extend(pipeline) self.config = self.config.copy() def tail(self, environ, start_response): """WSGI application callable for the actual CherryPy application. You probably shouldn't call this; call self.__call__ instead, so that any WSGI middleware in self.pipeline can run first. """ return self.response_class(environ, start_response, self.cpapp) def __call__(self, environ, start_response): head = self.head if head is None: # Create and nest the WSGI apps in our pipeline (in reverse order). # Then memoize the result in self.head. head = self.tail for name, callable in self.pipeline[::-1]: conf = self.config.get(name, {}) head = callable(head, **conf) self.head = head return head(environ, start_response) def namespace_handler(self, k, v): """Config handler for the 'wsgi' namespace.""" if k == 'pipeline': # Note this allows multiple 'wsgi.pipeline' config entries # (but each entry will be processed in a 'random' order). # It should also allow developers to set default middleware # in code (passed to self.__init__) that deployers can add to # (but not remove) via config. self.pipeline.extend(v) elif k == 'response_class': self.response_class = v else: name, arg = k.split('.', 1) bucket = self.config.setdefault(name, {}) bucket[arg] = v ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_cpwsgi_server.py0000644252176402575230000001013314307416152021471 0ustar00jaracoprimarygroup""" WSGI server interface (see PEP 333). This adds some CP-specific bits to the framework-agnostic cheroot package. """ import sys import cheroot.wsgi import cheroot.server import cherrypy class CPWSGIHTTPRequest(cheroot.server.HTTPRequest): """Wrapper for cheroot.server.HTTPRequest. This is a layer, which preserves URI parsing mode like it which was before Cheroot v5.8.0. """ def __init__(self, server, conn): """Initialize HTTP request container instance. Args: server (cheroot.server.HTTPServer): web server object receiving this request conn (cheroot.server.HTTPConnection): HTTP connection object for this request """ super(CPWSGIHTTPRequest, self).__init__( server, conn, proxy_mode=True ) class CPWSGIServer(cheroot.wsgi.Server): """Wrapper for cheroot.wsgi.Server. cheroot has been designed to not reference CherryPy in any way, so that it can be used in other frameworks and applications. Therefore, we wrap it here, so we can set our own mount points from cherrypy.tree and apply some attributes from config -> cherrypy.server -> wsgi.Server. """ fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}' version = fmt.format(**globals()) def __init__(self, server_adapter=cherrypy.server): """Initialize CPWSGIServer instance. Args: server_adapter (cherrypy._cpserver.Server): ... """ self.server_adapter = server_adapter self.max_request_header_size = ( self.server_adapter.max_request_header_size or 0 ) self.max_request_body_size = ( self.server_adapter.max_request_body_size or 0 ) server_name = (self.server_adapter.socket_host or self.server_adapter.socket_file or None) self.wsgi_version = self.server_adapter.wsgi_version super(CPWSGIServer, self).__init__( server_adapter.bind_addr, cherrypy.tree, self.server_adapter.thread_pool, server_name, max=self.server_adapter.thread_pool_max, request_queue_size=self.server_adapter.socket_queue_size, timeout=self.server_adapter.socket_timeout, shutdown_timeout=self.server_adapter.shutdown_timeout, accepted_queue_size=self.server_adapter.accepted_queue_size, accepted_queue_timeout=self.server_adapter.accepted_queue_timeout, peercreds_enabled=self.server_adapter.peercreds, peercreds_resolve_enabled=self.server_adapter.peercreds_resolve, ) self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest self.protocol = self.server_adapter.protocol_version self.nodelay = self.server_adapter.nodelay if sys.version_info >= (3, 0): ssl_module = self.server_adapter.ssl_module or 'builtin' else: ssl_module = self.server_adapter.ssl_module or 'pyopenssl' if self.server_adapter.ssl_context: adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) self.ssl_adapter = adapter_class( self.server_adapter.ssl_certificate, self.server_adapter.ssl_private_key, self.server_adapter.ssl_certificate_chain, self.server_adapter.ssl_ciphers) self.ssl_adapter.context = self.server_adapter.ssl_context elif self.server_adapter.ssl_certificate: adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) self.ssl_adapter = adapter_class( self.server_adapter.ssl_certificate, self.server_adapter.ssl_private_key, self.server_adapter.ssl_certificate_chain, self.server_adapter.ssl_ciphers) self.stats['Enabled'] = getattr( self.server_adapter, 'statistics', False) def error_log(self, msg='', level=20, traceback=False): """Write given message to the error log.""" cherrypy.engine.log(msg, level, traceback) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_helper.py0000644252176402575230000002660514307416152020101 0ustar00jaracoprimarygroup"""Helper functions for CP apps.""" import urllib.parse from cherrypy._cpcompat import text_or_bytes import cherrypy def expose(func=None, alias=None): """Expose the function or class. Optionally provide an alias or set of aliases. """ def expose_(func): func.exposed = True if alias is not None: if isinstance(alias, text_or_bytes): parents[alias.replace('.', '_')] = func else: for a in alias: parents[a.replace('.', '_')] = func return func import sys import types decoratable_types = types.FunctionType, types.MethodType, type, if isinstance(func, decoratable_types): if alias is None: # @expose func.exposed = True return func else: # func = expose(func, alias) parents = sys._getframe(1).f_locals return expose_(func) elif func is None: if alias is None: # @expose() parents = sys._getframe(1).f_locals return expose_ else: # @expose(alias="alias") or # @expose(alias=["alias1", "alias2"]) parents = sys._getframe(1).f_locals return expose_ else: # @expose("alias") or # @expose(["alias1", "alias2"]) parents = sys._getframe(1).f_locals alias = func return expose_ def popargs(*args, **kwargs): """Decorate _cp_dispatch. (cherrypy.dispatch.Dispatcher.dispatch_method_name) Optional keyword argument: handler=(Object or Function) Provides a _cp_dispatch function that pops off path segments into cherrypy.request.params under the names specified. The dispatch is then forwarded on to the next vpath element. Note that any existing (and exposed) member function of the class that popargs is applied to will override that value of the argument. For instance, if you have a method named "list" on the class decorated with popargs, then accessing "/list" will call that function instead of popping it off as the requested parameter. This restriction applies to all _cp_dispatch functions. The only way around this restriction is to create a "blank class" whose only function is to provide _cp_dispatch. If there are path elements after the arguments, or more arguments are requested than are available in the vpath, then the 'handler' keyword argument specifies the next object to handle the parameterized request. If handler is not specified or is None, then self is used. If handler is a function rather than an instance, then that function will be called with the args specified and the return value from that function used as the next object INSTEAD of adding the parameters to cherrypy.request.args. This decorator may be used in one of two ways: As a class decorator: .. code-block:: python @cherrypy.popargs('year', 'month', 'day') class Blog: def index(self, year=None, month=None, day=None): #Process the parameters here; any url like #/, /2009, /2009/12, or /2009/12/31 #will fill in the appropriate parameters. def create(self): #This link will still be available at /create. #Defined functions take precedence over arguments. Or as a member of a class: .. code-block:: python class Blog: _cp_dispatch = cherrypy.popargs('year', 'month', 'day') #... The handler argument may be used to mix arguments with built in functions. For instance, the following setup allows different activities at the day, month, and year level: .. code-block:: python class DayHandler: def index(self, year, month, day): #Do something with this day; probably list entries def delete(self, year, month, day): #Delete all entries for this day @cherrypy.popargs('day', handler=DayHandler()) class MonthHandler: def index(self, year, month): #Do something with this month; probably list entries def delete(self, year, month): #Delete all entries for this month @cherrypy.popargs('month', handler=MonthHandler()) class YearHandler: def index(self, year): #Do something with this year #... @cherrypy.popargs('year', handler=YearHandler()) class Root: def index(self): #... """ # Since keyword arg comes after *args, we have to process it ourselves # for lower versions of python. handler = None handler_call = False for k, v in kwargs.items(): if k == 'handler': handler = v else: tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'" raise TypeError(tm.format(k)) import inspect if handler is not None \ and (hasattr(handler, '__call__') or inspect.isclass(handler)): handler_call = True def decorated(cls_or_self=None, vpath=None): if inspect.isclass(cls_or_self): # cherrypy.popargs is a class decorator cls = cls_or_self name = cherrypy.dispatch.Dispatcher.dispatch_method_name setattr(cls, name, decorated) return cls # We're in the actual function self = cls_or_self parms = {} for arg in args: if not vpath: break parms[arg] = vpath.pop(0) if handler is not None: if handler_call: return handler(**parms) else: cherrypy.request.params.update(parms) return handler cherrypy.request.params.update(parms) # If we are the ultimate handler, then to prevent our _cp_dispatch # from being called again, we will resolve remaining elements through # getattr() directly. if vpath: return getattr(self, vpath.pop(0), None) else: return self return decorated def url(path='', qs='', script_name=None, base=None, relative=None): """Create an absolute URL for the given path. If 'path' starts with a slash ('/'), this will return (base + script_name + path + qs). If it does not start with a slash, this returns (base + script_name [+ request.path_info] + path + qs). If script_name is None, cherrypy.request will be used to find a script_name, if available. If base is None, cherrypy.request.base will be used (if available). Note that you can use cherrypy.tools.proxy to change this. Finally, note that this function can be used to obtain an absolute URL for the current request path (minus the querystring) by passing no args. If you call url(qs=cherrypy.request.query_string), you should get the original browser URL (assuming no internal redirections). If relative is None or not provided, request.app.relative_urls will be used (if available, else False). If False, the output will be an absolute URL (including the scheme, host, vhost, and script_name). If True, the output will instead be a URL that is relative to the current request path, perhaps including '..' atoms. If relative is the string 'server', the output will instead be a URL that is relative to the server root; i.e., it will start with a slash. """ if isinstance(qs, (tuple, list, dict)): qs = urllib.parse.urlencode(qs) if qs: qs = '?' + qs if cherrypy.request.app: if not path.startswith('/'): # Append/remove trailing slash from path_info as needed # (this is to support mistyped URL's without redirecting; # if you want to redirect, use tools.trailing_slash). pi = cherrypy.request.path_info if cherrypy.request.is_index is True: if not pi.endswith('/'): pi = pi + '/' elif cherrypy.request.is_index is False: if pi.endswith('/') and pi != '/': pi = pi[:-1] if path == '': path = pi else: path = urllib.parse.urljoin(pi, path) if script_name is None: script_name = cherrypy.request.script_name if base is None: base = cherrypy.request.base newurl = base + script_name + normalize_path(path) + qs else: # No request.app (we're being called outside a request). # We'll have to guess the base from server.* attributes. # This will produce very different results from the above # if you're using vhosts or tools.proxy. if base is None: base = cherrypy.server.base() path = (script_name or '') + path newurl = base + normalize_path(path) + qs # At this point, we should have a fully-qualified absolute URL. if relative is None: relative = getattr(cherrypy.request.app, 'relative_urls', False) # See http://www.ietf.org/rfc/rfc2396.txt if relative == 'server': # "A relative reference beginning with a single slash character is # termed an absolute-path reference, as defined by ..." # This is also sometimes called "server-relative". newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) elif relative: # "A relative reference that does not begin with a scheme name # or a slash character is termed a relative-path reference." old = url(relative=False).split('/')[:-1] new = newurl.split('/') while old and new: a, b = old[0], new[0] if a != b: break old.pop(0) new.pop(0) new = (['..'] * len(old)) + new newurl = '/'.join(new) return newurl def normalize_path(path): """Resolve given path from relative into absolute form.""" if './' not in path: return path # Normalize the URL by removing ./ and ../ atoms = [] for atom in path.split('/'): if atom == '.': pass elif atom == '..': # Don't pop from empty list # (i.e. ignore redundant '..') if atoms: atoms.pop() elif atom: atoms.append(atom) newpath = '/'.join(atoms) # Preserve leading '/' if path.startswith('/'): newpath = '/' + newpath return newpath #### # Inlined from jaraco.classes 1.4.3 # Ref #1673 class _ClassPropertyDescriptor(object): """Descript for read-only class-based property. Turns a classmethod-decorated func into a read-only property of that class type (means the value cannot be set). """ def __init__(self, fget, fset=None): """Initialize a class property descriptor. Instantiated by ``_helper.classproperty``. """ self.fget = fget self.fset = fset def __get__(self, obj, klass=None): """Return property value.""" if klass is None: klass = type(obj) return self.fget.__get__(obj, klass)() def classproperty(func): # noqa: D401; irrelevant for properties """Decorator like classmethod to implement a static class property.""" if not isinstance(func, (classmethod, staticmethod)): func = classmethod(func) return _ClassPropertyDescriptor(func) #### ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/_json.py0000644252176402575230000000067014307416152017565 0ustar00jaracoprimarygroup""" JSON support. Expose preferred json module as json and provide encode/decode convenience functions. """ try: # Prefer simplejson import simplejson as json except ImportError: import json __all__ = ['json', 'encode', 'decode'] decode = json.JSONDecoder().decode _encode = json.JSONEncoder().iterencode def encode(value): """Encode to bytes.""" for chunk in _encode(value): yield chunk.encode('utf-8') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/daemon.py0000755252176402575230000000755614307416152017735 0ustar00jaracoprimarygroup"""The CherryPy daemon.""" import sys import cherrypy from cherrypy.process import plugins, servers from cherrypy import Application def start(configfiles=None, daemonize=False, environment=None, fastcgi=False, scgi=False, pidfile=None, imports=None, cgi=False): """Subscribe all engine plugins and start the engine.""" sys.path = [''] + sys.path for i in imports or []: exec('import %s' % i) for c in configfiles or []: cherrypy.config.update(c) # If there's only one app mounted, merge config into it. if len(cherrypy.tree.apps) == 1: for app in cherrypy.tree.apps.values(): if isinstance(app, Application): app.merge(c) engine = cherrypy.engine if environment is not None: cherrypy.config.update({'environment': environment}) # Only daemonize if asked to. if daemonize: # Don't print anything to stdout/sterr. cherrypy.config.update({'log.screen': False}) plugins.Daemonizer(engine).subscribe() if pidfile: plugins.PIDFile(engine, pidfile).subscribe() if hasattr(engine, 'signal_handler'): engine.signal_handler.subscribe() if hasattr(engine, 'console_control_handler'): engine.console_control_handler.subscribe() if (fastcgi and (scgi or cgi)) or (scgi and cgi): cherrypy.log.error('You may only specify one of the cgi, fastcgi, and ' 'scgi options.', 'ENGINE') sys.exit(1) elif fastcgi or scgi or cgi: # Turn off autoreload when using *cgi. cherrypy.config.update({'engine.autoreload.on': False}) # Turn off the default HTTP server (which is subscribed by default). cherrypy.server.unsubscribe() addr = cherrypy.server.bind_addr cls = ( servers.FlupFCGIServer if fastcgi else servers.FlupSCGIServer if scgi else servers.FlupCGIServer ) f = cls(application=cherrypy.tree, bindAddress=addr) s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) s.subscribe() # Always start the engine; this will start all other services try: engine.start() except Exception: # Assume the error has been logged already via bus.log. sys.exit(1) else: engine.block() def run(): """Run cherryd CLI.""" from optparse import OptionParser p = OptionParser() p.add_option('-c', '--config', action='append', dest='config', help='specify config file(s)') p.add_option('-d', action='store_true', dest='daemonize', help='run the server as a daemon') p.add_option('-e', '--environment', dest='environment', default=None, help='apply the given config environment') p.add_option('-f', action='store_true', dest='fastcgi', help='start a fastcgi server instead of the default HTTP ' 'server') p.add_option('-s', action='store_true', dest='scgi', help='start a scgi server instead of the default HTTP server') p.add_option('-x', action='store_true', dest='cgi', help='start a cgi server instead of the default HTTP server') p.add_option('-i', '--import', action='append', dest='imports', help='specify modules to import') p.add_option('-p', '--pidfile', dest='pidfile', default=None, help='store the process id in the given file') p.add_option('-P', '--Path', action='append', dest='Path', help='add the given paths to sys.path') options, args = p.parse_args() if options.Path: for p in options.Path: sys.path.insert(0, p) start(options.config, options.daemonize, options.environment, options.fastcgi, options.scgi, options.pidfile, options.imports, options.cgi) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/favicon.ico0000644252176402575230000000257614307416152020233 0ustar00jaracoprimarygrouph( HHm$5- pjVʼhc[XRMj|-&D=<|***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ '"   %#!      $ (  )  & "& &././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5270617 CherryPy-18.9.0/cherrypy/lib/0000755252176402575230000000000014536340235016650 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/__init__.py0000644252176402575230000000527114307416152020764 0ustar00jaracoprimarygroup"""CherryPy Library.""" def is_iterator(obj): """Detect if the object provided implements the iterator protocol. (i.e. like a generator). This will return False for objects which are iterable, but not iterators themselves. """ from types import GeneratorType if isinstance(obj, GeneratorType): return True elif not hasattr(obj, '__iter__'): return False else: # Types which implement the protocol must return themselves when # invoking 'iter' upon them. return iter(obj) is obj def is_closable_iterator(obj): """Detect if the given object is both closable and iterator.""" # Not an iterator. if not is_iterator(obj): return False # A generator - the easiest thing to deal with. import inspect if inspect.isgenerator(obj): return True # A custom iterator. Look for a close method... if not (hasattr(obj, 'close') and callable(obj.close)): return False # ... which doesn't require any arguments. try: inspect.getcallargs(obj.close) except TypeError: return False else: return True class file_generator(object): """Yield the given input (a file object) in chunks (default 64k). (Core) """ def __init__(self, input, chunkSize=65536): """Initialize file_generator with file ``input`` for chunked access.""" self.input = input self.chunkSize = chunkSize def __iter__(self): """Return iterator.""" return self def __next__(self): """Return next chunk of file.""" chunk = self.input.read(self.chunkSize) if chunk: return chunk else: if hasattr(self.input, 'close'): self.input.close() raise StopIteration() next = __next__ def __del__(self): """Close input on descturct.""" if hasattr(self.input, 'close'): self.input.close() def file_generator_limited(fileobj, count, chunk_size=65536): """Yield the given file object in chunks. Stopps after `count` bytes has been emitted. Default chunk size is 64kB. (Core) """ remaining = count while remaining > 0: chunk = fileobj.read(min(chunk_size, remaining)) chunklen = len(chunk) if chunklen == 0: return remaining -= chunklen yield chunk def set_vary_header(response, header_name): """Add a Vary header to a response.""" varies = response.headers.get('Vary', '') varies = [x.strip() for x in varies.split(',') if x.strip()] if header_name not in varies: varies.append(header_name) response.headers['Vary'] = ', '.join(varies) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/auth_basic.py0000644252176402575230000001050514307416152021323 0ustar00jaracoprimarygroup# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 """HTTP Basic Authentication tool. This module provides a CherryPy 3.x tool which implements the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`. Example usage, using the built-in checkpassword_dict function which uses a dict as the credentials store:: userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) basic_auth = {'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'earth', 'tools.auth_basic.checkpassword': checkpassword, 'tools.auth_basic.accept_charset': 'UTF-8', } app_config = { '/' : basic_auth } """ import binascii import unicodedata import base64 import cherrypy from cherrypy._cpcompat import ntou, tonative __author__ = 'visteya' __date__ = 'April 2009' def checkpassword_dict(user_password_dict): """Returns a checkpassword function which checks credentials against a dictionary of the form: {username : password}. If you want a simple dictionary-based authentication scheme, use checkpassword_dict(my_credentials_dict) as the value for the checkpassword argument to basic_auth(). """ def checkpassword(realm, user, password): p = user_password_dict.get(user) return p and p == password or False return checkpassword def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'): """A CherryPy tool which hooks at before_handler to perform HTTP Basic Access Authentication, as specified in :rfc:`2617` and :rfc:`7617`. If the request has an 'authorization' header with a 'Basic' scheme, this tool attempts to authenticate the credentials supplied in that header. If the request has no 'authorization' header, or if it does but the scheme is not 'Basic', or if authentication fails, the tool sends a 401 response with a 'WWW-Authenticate' Basic header. realm A string containing the authentication realm. checkpassword A callable which checks the authentication credentials. Its signature is checkpassword(realm, username, password). where username and password are the values obtained from the request's 'authorization' header. If authentication succeeds, checkpassword returns True, else it returns False. """ fallback_charset = 'ISO-8859-1' if '"' in realm: raise ValueError('Realm cannot contain the " (quote) character.') request = cherrypy.serving.request auth_header = request.headers.get('authorization') if auth_header is not None: # split() error, base64.decodestring() error msg = 'Bad Request' with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg): scheme, params = auth_header.split(' ', 1) if scheme.lower() == 'basic': charsets = accept_charset, fallback_charset decoded_params = base64.b64decode(params.encode('ascii')) decoded_params = _try_decode(decoded_params, charsets) decoded_params = ntou(decoded_params) decoded_params = unicodedata.normalize('NFC', decoded_params) decoded_params = tonative(decoded_params) username, password = decoded_params.split(':', 1) if checkpassword(realm, username, password): if debug: cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') request.login = username return # successful authentication charset = accept_charset.upper() charset_declaration = ( (', charset="%s"' % charset) if charset != fallback_charset else '' ) # Respond with 401 status and a WWW-Authenticate header cherrypy.serving.response.headers['www-authenticate'] = ( 'Basic realm="%s"%s' % (realm, charset_declaration) ) raise cherrypy.HTTPError( 401, 'You are not authorized to access that resource') def _try_decode(subject, charsets): for charset in charsets[:-1]: try: return tonative(subject, charset) except ValueError: pass return tonative(subject, charsets[-1]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/auth_digest.py0000644252176402575230000003576714307416152021542 0ustar00jaracoprimarygroup# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 """HTTP Digest Authentication tool. An implementation of the server-side of HTTP Digest Access Authentication, which is described in :rfc:`2617`. Example usage, using the built-in get_ha1_dict_plain function which uses a dict of plaintext passwords as the credentials store:: userpassdict = {'alice' : '4x5istwelve'} get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) digest_auth = {'tools.auth_digest.on': True, 'tools.auth_digest.realm': 'wonderland', 'tools.auth_digest.get_ha1': get_ha1, 'tools.auth_digest.key': 'a565c27146791cfb', 'tools.auth_digest.accept_charset': 'UTF-8', } app_config = { '/' : digest_auth } """ import time import functools from hashlib import md5 from urllib.request import parse_http_list, parse_keqv_list import cherrypy from cherrypy._cpcompat import ntob, tonative __author__ = 'visteya' __date__ = 'April 2009' def md5_hex(s): return md5(ntob(s, 'utf-8')).hexdigest() qop_auth = 'auth' qop_auth_int = 'auth-int' valid_qops = (qop_auth, qop_auth_int) valid_algorithms = ('MD5', 'MD5-sess') FALLBACK_CHARSET = 'ISO-8859-1' DEFAULT_CHARSET = 'UTF-8' def TRACE(msg): cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') # Three helper functions for users of the tool, providing three variants # of get_ha1() functions for three different kinds of credential stores. def get_ha1_dict_plain(user_password_dict): """Returns a get_ha1 function which obtains a plaintext password from a dictionary of the form: {username : password}. If you want a simple dictionary-based authentication scheme, with plaintext passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the get_ha1 argument to digest_auth(). """ def get_ha1(realm, username): password = user_password_dict.get(username) if password: return md5_hex('%s:%s:%s' % (username, realm, password)) return None return get_ha1 def get_ha1_dict(user_ha1_dict): """Returns a get_ha1 function which obtains a HA1 password hash from a dictionary of the form: {username : HA1}. If you want a dictionary-based authentication scheme, but with pre-computed HA1 hashes instead of plain-text passwords, use get_ha1_dict(my_userha1_dict) as the value for the get_ha1 argument to digest_auth(). """ def get_ha1(realm, username): return user_ha1_dict.get(username) return get_ha1 def get_ha1_file_htdigest(filename): """Returns a get_ha1 function which obtains a HA1 password hash from a flat file with lines of the same format as that produced by the Apache htdigest utility. For example, for realm 'wonderland', username 'alice', and password '4x5istwelve', the htdigest line would be:: alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c If you want to use an Apache htdigest file as the credentials store, then use get_ha1_file_htdigest(my_htdigest_file) as the value for the get_ha1 argument to digest_auth(). It is recommended that the filename argument be an absolute path, to avoid problems. """ def get_ha1(realm, username): result = None with open(filename, 'r') as f: for line in f: u, r, ha1 = line.rstrip().split(':') if u == username and r == realm: result = ha1 break return result return get_ha1 def synthesize_nonce(s, key, timestamp=None): """Synthesize a nonce value which resists spoofing and can be checked for staleness. Returns a string suitable as the value for 'nonce' in the www-authenticate header. s A string related to the resource, such as the hostname of the server. key A secret string known only to the server. timestamp An integer seconds-since-the-epoch timestamp """ if timestamp is None: timestamp = int(time.time()) h = md5_hex('%s:%s:%s' % (timestamp, s, key)) nonce = '%s:%s' % (timestamp, h) return nonce def H(s): """The hash function H""" return md5_hex(s) def _try_decode_header(header, charset): global FALLBACK_CHARSET for enc in (charset, FALLBACK_CHARSET): try: return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc) except ValueError as ve: last_err = ve else: raise last_err class HttpDigestAuthorization(object): """ Parses a Digest Authorization header and performs re-calculation of the digest. """ scheme = 'digest' def errmsg(self, s): return 'Digest Authorization header: %s' % s @classmethod def matches(cls, header): scheme, _, _ = header.partition(' ') return scheme.lower() == cls.scheme def __init__( self, auth_header, http_method, debug=False, accept_charset=DEFAULT_CHARSET[:], ): self.http_method = http_method self.debug = debug if not self.matches(auth_header): raise ValueError('Authorization scheme is not "Digest"') self.auth_header = _try_decode_header(auth_header, accept_charset) scheme, params = self.auth_header.split(' ', 1) # make a dict of the params items = parse_http_list(params) paramsd = parse_keqv_list(items) self.realm = paramsd.get('realm') self.username = paramsd.get('username') self.nonce = paramsd.get('nonce') self.uri = paramsd.get('uri') self.method = paramsd.get('method') self.response = paramsd.get('response') # the response digest self.algorithm = paramsd.get('algorithm', 'MD5').upper() self.cnonce = paramsd.get('cnonce') self.opaque = paramsd.get('opaque') self.qop = paramsd.get('qop') # qop self.nc = paramsd.get('nc') # nonce count # perform some correctness checks if self.algorithm not in valid_algorithms: raise ValueError( self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm)) has_reqd = ( self.username and self.realm and self.nonce and self.uri and self.response ) if not has_reqd: raise ValueError( self.errmsg('Not all required parameters are present.')) if self.qop: if self.qop not in valid_qops: raise ValueError( self.errmsg("Unsupported value for qop: '%s'" % self.qop)) if not (self.cnonce and self.nc): raise ValueError( self.errmsg('If qop is sent then ' 'cnonce and nc MUST be present')) else: if self.cnonce or self.nc: raise ValueError( self.errmsg('If qop is not sent, ' 'neither cnonce nor nc can be present')) def __str__(self): return 'authorization : %s' % self.auth_header def validate_nonce(self, s, key): """Validate the nonce. Returns True if nonce was generated by synthesize_nonce() and the timestamp is not spoofed, else returns False. s A string related to the resource, such as the hostname of the server. key A secret string known only to the server. Both s and key must be the same values which were used to synthesize the nonce we are trying to validate. """ try: timestamp, hashpart = self.nonce.split(':', 1) s_timestamp, s_hashpart = synthesize_nonce( s, key, timestamp).split(':', 1) is_valid = s_hashpart == hashpart if self.debug: TRACE('validate_nonce: %s' % is_valid) return is_valid except ValueError: # split() error pass return False def is_nonce_stale(self, max_age_seconds=600): """Returns True if a validated nonce is stale. The nonce contains a timestamp in plaintext and also a secure hash of the timestamp. You should first validate the nonce to ensure the plaintext timestamp is not spoofed. """ try: timestamp, hashpart = self.nonce.split(':', 1) if int(timestamp) + max_age_seconds > int(time.time()): return False except ValueError: # int() error pass if self.debug: TRACE('nonce is stale') return True def HA2(self, entity_body=''): """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" # RFC 2617 3.2.2.3 # If the "qop" directive's value is "auth" or is unspecified, # then A2 is: # A2 = method ":" digest-uri-value # # If the "qop" value is "auth-int", then A2 is: # A2 = method ":" digest-uri-value ":" H(entity-body) if self.qop is None or self.qop == 'auth': a2 = '%s:%s' % (self.http_method, self.uri) elif self.qop == 'auth-int': a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body)) else: # in theory, this should never happen, since I validate qop in # __init__() raise ValueError(self.errmsg('Unrecognized value for qop!')) return H(a2) def request_digest(self, ha1, entity_body=''): """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. ha1 The HA1 string obtained from the credentials store. entity_body If 'qop' is set to 'auth-int', then A2 includes a hash of the "entity body". The entity body is the part of the message which follows the HTTP headers. See :rfc:`2617` section 4.3. This refers to the entity the user agent sent in the request which has the Authorization header. Typically GET requests don't have an entity, and POST requests do. """ ha2 = self.HA2(entity_body) # Request-Digest -- RFC 2617 3.2.2.1 if self.qop: req = '%s:%s:%s:%s:%s' % ( self.nonce, self.nc, self.cnonce, self.qop, ha2) else: req = '%s:%s' % (self.nonce, ha2) # RFC 2617 3.2.2.2 # # If the "algorithm" directive's value is "MD5" or is unspecified, # then A1 is: # A1 = unq(username-value) ":" unq(realm-value) ":" passwd # # If the "algorithm" directive's value is "MD5-sess", then A1 is # calculated only once - on the first request by the client following # receipt of a WWW-Authenticate challenge from the server. # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) # ":" unq(nonce-value) ":" unq(cnonce-value) if self.algorithm == 'MD5-sess': ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) digest = H('%s:%s' % (ha1, req)) return digest def _get_charset_declaration(charset): global FALLBACK_CHARSET charset = charset.upper() return ( (', charset="%s"' % charset) if charset != FALLBACK_CHARSET else '' ) def www_authenticate( realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False, accept_charset=DEFAULT_CHARSET[:], ): """Constructs a WWW-Authenticate header for Digest authentication.""" if qop not in valid_qops: raise ValueError("Unsupported value for qop: '%s'" % qop) if algorithm not in valid_algorithms: raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) HEADER_PATTERN = ( 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s' ) if nonce is None: nonce = synthesize_nonce(realm, key) stale_param = ', stale="true"' if stale else '' charset_declaration = _get_charset_declaration(accept_charset) return HEADER_PATTERN % ( realm, nonce, algorithm, qop, stale_param, charset_declaration, ) def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'): """A CherryPy tool that hooks at before_handler to perform HTTP Digest Access Authentication, as specified in :rfc:`2617`. If the request has an 'authorization' header with a 'Digest' scheme, this tool authenticates the credentials supplied in that header. If the request has no 'authorization' header, or if it does but the scheme is not "Digest", or if authentication fails, the tool sends a 401 response with a 'WWW-Authenticate' Digest header. realm A string containing the authentication realm. get_ha1 A callable that looks up a username in a credentials store and returns the HA1 string, which is defined in the RFC to be MD5(username : realm : password). The function's signature is: ``get_ha1(realm, username)`` where username is obtained from the request's 'authorization' header. If username is not found in the credentials store, get_ha1() returns None. key A secret string known only to the server, used in the synthesis of nonces. """ request = cherrypy.serving.request auth_header = request.headers.get('authorization') respond_401 = functools.partial( _respond_401, realm, key, accept_charset, debug) if not HttpDigestAuthorization.matches(auth_header or ''): respond_401() msg = 'The Authorization header could not be parsed.' with cherrypy.HTTPError.handle(ValueError, 400, msg): auth = HttpDigestAuthorization( auth_header, request.method, debug=debug, accept_charset=accept_charset, ) if debug: TRACE(str(auth)) if not auth.validate_nonce(realm, key): respond_401() ha1 = get_ha1(realm, auth.username) if ha1 is None: respond_401() # note that for request.body to be available we need to # hook in at before_handler, not on_start_resource like # 3.1.x digest_auth does. digest = auth.request_digest(ha1, entity_body=request.body) if digest != auth.response: respond_401() # authenticated if debug: TRACE('digest matches auth.response') # Now check if nonce is stale. # The choice of ten minutes' lifetime for nonce is somewhat # arbitrary if auth.is_nonce_stale(max_age_seconds=600): respond_401(stale=True) request.login = auth.username if debug: TRACE('authentication of %s successful' % auth.username) def _respond_401(realm, key, accept_charset, debug, **kwargs): """ Respond with 401 status and a WWW-Authenticate header """ header = www_authenticate( realm, key, accept_charset=accept_charset, **kwargs ) if debug: TRACE(header) cherrypy.serving.response.headers['WWW-Authenticate'] = header raise cherrypy.HTTPError( 401, 'You are not authorized to access that resource') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/caching.py0000644252176402575230000004215514307416152020623 0ustar00jaracoprimarygroup""" CherryPy implements a simple caching system as a pluggable Tool. This tool tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but it's probably good enough for most sites. In general, GET responses are cached (along with selecting headers) and, if another request arrives for the same resource, the caching Tool will return 304 Not Modified if possible, or serve the cached response otherwise. It also sets request.cached to True if serving a cached representation, and sets request.cacheable to False (so it doesn't get cached again). If POST, PUT, or DELETE requests are made for a cached resource, they invalidate (delete) any cached response. Usage ===== Configuration file example:: [/] tools.caching.on = True tools.caching.delay = 3600 You may use a class other than the default :class:`MemoryCache` by supplying the config entry ``cache_class``; supply the full dotted name of the replacement class as the config value. It must implement the basic methods ``get``, ``put``, ``delete``, and ``clear``. You may set any attribute, including overriding methods, on the cache instance by providing them in config. The above sets the :attr:`delay` attribute, for example. """ import datetime import sys import threading import time import cherrypy from cherrypy.lib import cptools, httputil class Cache(object): """Base class for Cache implementations.""" def get(self): """Return the current variant if in the cache, else None.""" raise NotImplementedError def put(self, obj, size): """Store the current variant in the cache.""" raise NotImplementedError def delete(self): """Remove ALL cached variants of the current resource.""" raise NotImplementedError def clear(self): """Reset the cache to its initial, empty state.""" raise NotImplementedError # ------------------------------ Memory Cache ------------------------------- # class AntiStampedeCache(dict): """A storage system for cached items which reduces stampede collisions.""" def wait(self, key, timeout=5, debug=False): """Return the cached value for the given key, or None. If timeout is not None, and the value is already being calculated by another thread, wait until the given timeout has elapsed. If the value is available before the timeout expires, it is returned. If not, None is returned, and a sentinel placed in the cache to signal other threads to wait. If timeout is None, no waiting is performed nor sentinels used. """ value = self.get(key) if isinstance(value, threading.Event): if timeout is None: # Ignore the other thread and recalc it ourselves. if debug: cherrypy.log('No timeout', 'TOOLS.CACHING') return None # Wait until it's done or times out. if debug: cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING') value.wait(timeout) if value.result is not None: # The other thread finished its calculation. Use it. if debug: cherrypy.log('Result!', 'TOOLS.CACHING') return value.result # Timed out. Stick an Event in the slot so other threads wait # on this one to finish calculating the value. if debug: cherrypy.log('Timed out', 'TOOLS.CACHING') e = threading.Event() e.result = None dict.__setitem__(self, key, e) return None elif value is None: # Stick an Event in the slot so other threads wait # on this one to finish calculating the value. if debug: cherrypy.log('Timed out', 'TOOLS.CACHING') e = threading.Event() e.result = None dict.__setitem__(self, key, e) return value def __setitem__(self, key, value): """Set the cached value for the given key.""" existing = self.get(key) dict.__setitem__(self, key, value) if isinstance(existing, threading.Event): # Set Event.result so other threads waiting on it have # immediate access without needing to poll the cache again. existing.result = value existing.set() class MemoryCache(Cache): """An in-memory cache for varying response content. Each key in self.store is a URI, and each value is an AntiStampedeCache. The response for any given URI may vary based on the values of "selecting request headers"; that is, those named in the Vary response header. We assume the list of header names to be constant for each URI throughout the lifetime of the application, and store that list in ``self.store[uri].selecting_headers``. The items contained in ``self.store[uri]`` have keys which are tuples of request header values (in the same order as the names in its selecting_headers), and values which are the actual responses. """ maxobjects = 1000 """The maximum number of cached objects; defaults to 1000.""" maxobj_size = 100000 """The maximum size of each cached object in bytes; defaults to 100 KB.""" maxsize = 10000000 """The maximum size of the entire cache in bytes; defaults to 10 MB.""" delay = 600 """Seconds until the cached content expires; defaults to 600 (10 minutes). """ antistampede_timeout = 5 """Seconds to wait for other threads to release a cache lock.""" expire_freq = 0.1 """Seconds to sleep between cache expiration sweeps.""" debug = False def __init__(self): self.clear() # Run self.expire_cache in a separate daemon thread. t = threading.Thread(target=self.expire_cache, name='expire_cache') self.expiration_thread = t t.daemon = True t.start() def clear(self): """Reset the cache to its initial, empty state.""" self.store = {} self.expirations = {} self.tot_puts = 0 self.tot_gets = 0 self.tot_hist = 0 self.tot_expires = 0 self.tot_non_modified = 0 self.cursize = 0 def expire_cache(self): """Continuously examine cached objects, expiring stale ones. This function is designed to be run in its own daemon thread, referenced at ``self.expiration_thread``. """ # It's possible that "time" will be set to None # arbitrarily, so we check "while time" to avoid exceptions. # See tickets #99 and #180 for more information. while time: now = time.time() # Must make a copy of expirations so it doesn't change size # during iteration for expiration_time, objects in self.expirations.copy().items(): if expiration_time <= now: for obj_size, uri, sel_header_values in objects: try: del self.store[uri][tuple(sel_header_values)] self.tot_expires += 1 self.cursize -= obj_size except KeyError: # the key may have been deleted elsewhere pass del self.expirations[expiration_time] time.sleep(self.expire_freq) def get(self): """Return the current variant if in the cache, else None.""" request = cherrypy.serving.request self.tot_gets += 1 uri = cherrypy.url(qs=request.query_string) uricache = self.store.get(uri) if uricache is None: return None header_values = [request.headers.get(h, '') for h in uricache.selecting_headers] variant = uricache.wait(key=tuple(sorted(header_values)), timeout=self.antistampede_timeout, debug=self.debug) if variant is not None: self.tot_hist += 1 return variant def put(self, variant, size): """Store the current variant in the cache.""" request = cherrypy.serving.request response = cherrypy.serving.response uri = cherrypy.url(qs=request.query_string) uricache = self.store.get(uri) if uricache is None: uricache = AntiStampedeCache() uricache.selecting_headers = [ e.value for e in response.headers.elements('Vary')] self.store[uri] = uricache if len(self.store) < self.maxobjects: total_size = self.cursize + size # checks if there's space for the object if (size < self.maxobj_size and total_size < self.maxsize): # add to the expirations list expiration_time = response.time + self.delay bucket = self.expirations.setdefault(expiration_time, []) bucket.append((size, uri, uricache.selecting_headers)) # add to the cache header_values = [request.headers.get(h, '') for h in uricache.selecting_headers] uricache[tuple(sorted(header_values))] = variant self.tot_puts += 1 self.cursize = total_size def delete(self): """Remove ALL cached variants of the current resource.""" uri = cherrypy.url(qs=cherrypy.serving.request.query_string) self.store.pop(uri, None) def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs): """Try to obtain cached output. If fresh enough, raise HTTPError(304). If POST, PUT, or DELETE: * invalidates (deletes) any cached response for this resource * sets request.cached = False * sets request.cacheable = False else if a cached copy exists: * sets request.cached = True * sets request.cacheable = False * sets response.headers to the cached values * checks the cached Last-Modified response header against the current If-(Un)Modified-Since request headers; raises 304 if necessary. * sets response.status and response.body to the cached values * returns True otherwise: * sets request.cached = False * sets request.cacheable = True * returns False """ request = cherrypy.serving.request response = cherrypy.serving.response if not hasattr(cherrypy, '_cache'): # Make a process-wide Cache object. cherrypy._cache = kwargs.pop('cache_class', MemoryCache)() # Take all remaining kwargs and set them on the Cache object. for k, v in kwargs.items(): setattr(cherrypy._cache, k, v) cherrypy._cache.debug = debug # POST, PUT, DELETE should invalidate (delete) the cached copy. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. if request.method in invalid_methods: if debug: cherrypy.log('request.method %r in invalid_methods %r' % (request.method, invalid_methods), 'TOOLS.CACHING') cherrypy._cache.delete() request.cached = False request.cacheable = False return False if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: request.cached = False request.cacheable = True return False cache_data = cherrypy._cache.get() request.cached = bool(cache_data) request.cacheable = not request.cached if request.cached: # Serve the cached copy. max_age = cherrypy._cache.delay for v in [e.value for e in request.headers.elements('Cache-Control')]: atoms = v.split('=', 1) directive = atoms.pop(0) if directive == 'max-age': if len(atoms) != 1 or not atoms[0].isdigit(): raise cherrypy.HTTPError( 400, 'Invalid Cache-Control header') max_age = int(atoms[0]) break elif directive == 'no-cache': if debug: cherrypy.log( 'Ignoring cache due to Cache-Control: no-cache', 'TOOLS.CACHING') request.cached = False request.cacheable = True return False if debug: cherrypy.log('Reading response from cache', 'TOOLS.CACHING') s, h, b, create_time = cache_data age = int(response.time - create_time) if (age > max_age): if debug: cherrypy.log('Ignoring cache due to age > %d' % max_age, 'TOOLS.CACHING') request.cached = False request.cacheable = True return False # Copy the response headers. See # https://github.com/cherrypy/cherrypy/issues/721. response.headers = rh = httputil.HeaderMap() for k in h: dict.__setitem__(rh, k, dict.__getitem__(h, k)) # Add the required Age header response.headers['Age'] = str(age) try: # Note that validate_since depends on a Last-Modified header; # this was put into the cached copy, and should have been # resurrected just above (response.headers = cache_data[1]). cptools.validate_since() except cherrypy.HTTPRedirect: x = sys.exc_info()[1] if x.status == 304: cherrypy._cache.tot_non_modified += 1 raise # serve it & get out from the request response.status = s response.body = b else: if debug: cherrypy.log('request is not cached', 'TOOLS.CACHING') return request.cached def tee_output(): """Tee response output to cache storage. Internal.""" # Used by CachingTool by attaching to request.hooks request = cherrypy.serving.request if 'no-store' in request.headers.values('Cache-Control'): return def tee(body): """Tee response.body into a list.""" if ('no-cache' in response.headers.values('Pragma') or 'no-store' in response.headers.values('Cache-Control')): for chunk in body: yield chunk return output = [] for chunk in body: output.append(chunk) yield chunk # Save the cache data, but only if the body isn't empty. # e.g. a 304 Not Modified on a static file response will # have an empty body. # If the body is empty, delete the cache because it # contains a stale Threading._Event object that will # stall all consecutive requests until the _Event times # out body = b''.join(output) if not body: cherrypy._cache.delete() else: cherrypy._cache.put((response.status, response.headers or {}, body, response.time), len(body)) response = cherrypy.serving.response response.body = tee(response.body) def expires(secs=0, force=False, debug=False): """Tool for influencing cache mechanisms using the 'Expires' header. secs Must be either an int or a datetime.timedelta, and indicates the number of seconds between response.time and when the response should expire. The 'Expires' header will be set to response.time + secs. If secs is zero, the 'Expires' header is set one year in the past, and the following "cache prevention" headers are also set: * Pragma: no-cache * Cache-Control': no-cache, must-revalidate force If False, the following headers are checked: * Etag * Last-Modified * Age * Expires If any are already present, none of the above response headers are set. """ response = cherrypy.serving.response headers = response.headers cacheable = False if not force: # some header names that indicate that the response can be cached for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): if indicator in headers: cacheable = True break if not cacheable and not force: if debug: cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') else: if debug: cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') if isinstance(secs, datetime.timedelta): secs = (86400 * secs.days) + secs.seconds if secs == 0: if force or ('Pragma' not in headers): headers['Pragma'] = 'no-cache' if cherrypy.serving.request.protocol >= (1, 1): if force or 'Cache-Control' not in headers: headers['Cache-Control'] = 'no-cache, must-revalidate' # Set an explicit Expires date in the past. expiry = httputil.HTTPDate(1169942400.0) else: expiry = httputil.HTTPDate(response.time + secs) if force or 'Expires' not in headers: headers['Expires'] = expiry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/covercp.py0000644252176402575230000002651714307416152020674 0ustar00jaracoprimarygroup"""Code-coverage tools for CherryPy. To use this module, or the coverage tools in the test suite, you need to download 'coverage.py', either Gareth Rees' `original implementation `_ or Ned Batchelder's `enhanced version: `_ To turn on coverage tracing, use the following code:: cherrypy.engine.subscribe('start', covercp.start) DO NOT subscribe anything on the 'start_thread' channel, as previously recommended. Calling start once in the main thread should be sufficient to start coverage on all threads. Calling start again in each thread effectively clears any coverage data gathered up to that point. Run your code, then use the ``covercp.serve()`` function to browse the results in a web browser. If you run this module from the command line, it will call ``serve()`` for you. """ import re import sys import cgi import os import os.path import urllib.parse import cherrypy localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache') the_coverage = None try: from coverage import coverage the_coverage = coverage(data_file=localFile) def start(): the_coverage.start() except ImportError: # Setting the_coverage to None will raise errors # that need to be trapped downstream. the_coverage = None import warnings warnings.warn( 'No code coverage will be performed; ' 'coverage.py could not be imported.') def start(): pass start.priority = 20 TEMPLATE_MENU = """ CherryPy Coverage Menu

CherryPy Coverage

""" TEMPLATE_FORM = """
Show percentages
Hide files over %%
Exclude files matching

""" TEMPLATE_FRAMESET = """ CherryPy coverage data """ TEMPLATE_COVERAGE = """ Coverage for %(name)s

%(name)s

%(fullpath)s

Coverage: %(pc)s%%

""" TEMPLATE_LOC_COVERED = """ %s  %s \n""" TEMPLATE_LOC_NOT_COVERED = """ %s  %s \n""" TEMPLATE_LOC_EXCLUDED = """ %s  %s \n""" TEMPLATE_ITEM = ( "%s%s%s\n" ) def _percent(statements, missing): s = len(statements) e = s - len(missing) if s > 0: return int(round(100.0 * e / s)) return 0 def _show_branch(root, base, path, pct=0, showpct=False, exclude='', coverage=the_coverage): # Show the directory name and any of our children dirs = [k for k, v in root.items() if v] dirs.sort() for name in dirs: newpath = os.path.join(path, name) if newpath.lower().startswith(base): relpath = newpath[len(base):] yield '| ' * relpath.count(os.sep) yield ( "%s\n" % (newpath, urllib.parse.quote_plus(exclude), name) ) for chunk in _show_branch( root[name], base, newpath, pct, showpct, exclude, coverage=coverage ): yield chunk # Now list the files if path.lower().startswith(base): relpath = path[len(base):] files = [k for k, v in root.items() if not v] files.sort() for name in files: newpath = os.path.join(path, name) pc_str = '' if showpct: try: _, statements, _, missing, _ = coverage.analysis2(newpath) except Exception: # Yes, we really want to pass on all errors. pass else: pc = _percent(statements, missing) pc_str = ('%3d%% ' % pc).replace(' ', ' ') if pc < float(pct) or pc == -1: pc_str = "%s" % pc_str else: pc_str = "%s" % pc_str yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1), pc_str, newpath, name) def _skip_file(path, exclude): if exclude: return bool(re.search(exclude, path)) def _graft(path, tree): d = tree p = path atoms = [] while True: p, tail = os.path.split(p) if not tail: break atoms.append(tail) atoms.append(p) if p != '/': atoms.append('/') atoms.reverse() for node in atoms: if node: d = d.setdefault(node, {}) def get_tree(base, exclude, coverage=the_coverage): """Return covered module names as a nested dict.""" tree = {} runs = coverage.data.executed_files() for path in runs: if not _skip_file(path, exclude) and not os.path.isdir(path): _graft(path, tree) return tree class CoverStats(object): def __init__(self, coverage, root=None): self.coverage = coverage if root is None: # Guess initial depth. Files outside this path will not be # reachable from the web interface. root = os.path.dirname(cherrypy.__file__) self.root = root @cherrypy.expose def index(self): return TEMPLATE_FRAMESET % self.root.lower() @cherrypy.expose def menu(self, base='/', pct='50', showpct='', exclude=r'python\d\.\d|test|tut\d|tutorial'): # The coverage module uses all-lower-case names. base = base.lower().rstrip(os.sep) yield TEMPLATE_MENU yield TEMPLATE_FORM % locals() # Start by showing links for parent paths yield "
" path = '' atoms = base.split(os.sep) atoms.pop() for atom in atoms: path += atom + os.sep yield ("%s %s" % (path, urllib.parse.quote_plus(exclude), atom, os.sep)) yield '
' yield "
" # Then display the tree tree = get_tree(base, exclude, self.coverage) if not tree: yield '

No modules covered.

' else: for chunk in _show_branch(tree, base, '/', pct, showpct == 'checked', exclude, coverage=self.coverage): yield chunk yield '
' yield '' def annotated_file(self, filename, statements, excluded, missing): with open(filename, 'r') as source: lines = source.readlines() buffer = [] for lineno, line in enumerate(lines): lineno += 1 line = line.strip('\n\r') empty_the_buffer = True if lineno in excluded: template = TEMPLATE_LOC_EXCLUDED elif lineno in missing: template = TEMPLATE_LOC_NOT_COVERED elif lineno in statements: template = TEMPLATE_LOC_COVERED else: empty_the_buffer = False buffer.append((lineno, line)) if empty_the_buffer: for lno, pastline in buffer: yield template % (lno, cgi.escape(pastline)) buffer = [] yield template % (lineno, cgi.escape(line)) @cherrypy.expose def report(self, name): filename, statements, excluded, missing, _ = self.coverage.analysis2( name) pc = _percent(statements, missing) yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), fullpath=name, pc=pc) yield '\n' for line in self.annotated_file(filename, statements, excluded, missing): yield line yield '
' yield '' yield '' def serve(path=localFile, port=8080, root=None): if coverage is None: raise ImportError('The coverage module could not be imported.') from coverage import coverage cov = coverage(data_file=path) cov.load() cherrypy.config.update({'server.socket_port': int(port), 'server.thread_pool': 10, 'environment': 'production', }) cherrypy.quickstart(CoverStats(cov, root)) if __name__ == '__main__': serve(*tuple(sys.argv[1:])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/cpstats.py0000644252176402575230000005450614307416152020713 0ustar00jaracoprimarygroup"""CPStats, a package for collecting and reporting on program statistics. Overview ======== Statistics about program operation are an invaluable monitoring and debugging tool. Unfortunately, the gathering and reporting of these critical values is usually ad-hoc. This package aims to add a centralized place for gathering statistical performance data, a structure for recording that data which provides for extrapolation of that data into more useful information, and a method of serving that data to both human investigators and monitoring software. Let's examine each of those in more detail. Data Gathering -------------- Just as Python's `logging` module provides a common importable for gathering and sending messages, performance statistics would benefit from a similar common mechanism, and one that does *not* require each package which wishes to collect stats to import a third-party module. Therefore, we choose to re-use the `logging` module by adding a `statistics` object to it. That `logging.statistics` object is a nested dict. It is not a custom class, because that would: 1. require libraries and applications to import a third-party module in order to participate 2. inhibit innovation in extrapolation approaches and in reporting tools, and 3. be slow. There are, however, some specifications regarding the structure of the dict.:: { +----"SQLAlchemy": { | "Inserts": 4389745, | "Inserts per Second": | lambda s: s["Inserts"] / (time() - s["Start"]), | C +---"Table Statistics": { | o | "widgets": {-----------+ N | l | "Rows": 1.3M, | Record a | l | "Inserts": 400, | m | e | },---------------------+ e | c | "froobles": { s | t | "Rows": 7845, p | i | "Inserts": 0, a | o | }, c | n +---}, e | "Slow Queries": | [{"Query": "SELECT * FROM widgets;", | "Processing Time": 47.840923343, | }, | ], +----}, } The `logging.statistics` dict has four levels. The topmost level is nothing more than a set of names to introduce modularity, usually along the lines of package names. If the SQLAlchemy project wanted to participate, for example, it might populate the item `logging.statistics['SQLAlchemy']`, whose value would be a second-layer dict we call a "namespace". Namespaces help multiple packages to avoid collisions over key names, and make reports easier to read, to boot. The maintainers of SQLAlchemy should feel free to use more than one namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case or other syntax constraints on the namespace names; they should be chosen to be maximally readable by humans (neither too short nor too long). Each namespace, then, is a dict of named statistical values, such as 'Requests/sec' or 'Uptime'. You should choose names which will look good on a report: spaces and capitalization are just fine. In addition to scalars, values in a namespace MAY be a (third-layer) dict, or a list, called a "collection". For example, the CherryPy :class:`StatsTool` keeps track of what each request is doing (or has most recently done) in a 'Requests' collection, where each key is a thread ID; each value in the subdict MUST be a fourth dict (whew!) of statistical data about each thread. We call each subdict in the collection a "record". Similarly, the :class:`StatsTool` also keeps a list of slow queries, where each record contains data about each slow query, in order. Values in a namespace or record may also be functions, which brings us to: Extrapolation ------------- The collection of statistical data needs to be fast, as close to unnoticeable as possible to the host program. That requires us to minimize I/O, for example, but in Python it also means we need to minimize function calls. So when you are designing your namespace and record values, try to insert the most basic scalar values you already have on hand. When it comes time to report on the gathered data, however, we usually have much more freedom in what we can calculate. Therefore, whenever reporting tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents of `logging.statistics` for reporting, they first call `extrapolate_statistics` (passing the whole `statistics` dict as the only argument). This makes a deep copy of the statistics dict so that the reporting tool can both iterate over it and even change it without harming the original. But it also expands any functions in the dict by calling them. For example, you might have a 'Current Time' entry in the namespace with the value "lambda scope: time.time()". The "scope" parameter is the current namespace dict (or record, if we're currently expanding one of those instead), allowing you access to existing static entries. If you're truly evil, you can even modify more than one entry at a time. However, don't try to calculate an entry and then use its value in further extrapolations; the order in which the functions are called is not guaranteed. This can lead to a certain amount of duplicated work (or a redesign of your schema), but that's better than complicating the spec. After the whole thing has been extrapolated, it's time for: Reporting --------- The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates it all, and then transforms it to HTML for easy viewing. Each namespace gets its own header and attribute table, plus an extra table for each collection. This is NOT part of the statistics specification; other tools can format how they like. You can control which columns are output and how they are formatted by updating StatsPage.formatting, which is a dict that mirrors the keys and nesting of `logging.statistics`. The difference is that, instead of data values, it has formatting values. Use None for a given key to indicate to the StatsPage that a given column should not be output. Use a string with formatting (such as '%.3f') to interpolate the value(s), or use a callable (such as lambda v: v.isoformat()) for more advanced formatting. Any entry which is not mentioned in the formatting dict is output unchanged. Monitoring ---------- Although the HTML output takes pains to assign unique id's to each with statistical data, you're probably better off fetching /cpstats/data, which outputs the whole (extrapolated) `logging.statistics` dict in JSON format. That is probably easier to parse, and doesn't have any formatting controls, so you get the "original" data in a consistently-serialized format. Note: there's no treatment yet for datetime objects. Try time.time() instead for now if you can. Nagios will probably thank you. Turning Collection Off ---------------------- It is recommended each namespace have an "Enabled" item which, if False, stops collection (but not reporting) of statistical data. Applications SHOULD provide controls to pause and resume collection by setting these entries to False or True, if present. Usage ===== To collect statistics on CherryPy applications:: from cherrypy.lib import cpstats appconfig['/']['tools.cpstats.on'] = True To collect statistics on your own code:: import logging # Initialize the repository if not hasattr(logging, 'statistics'): logging.statistics = {} # Initialize my namespace mystats = logging.statistics.setdefault('My Stuff', {}) # Initialize my namespace's scalars and collections mystats.update({ 'Enabled': True, 'Start Time': time.time(), 'Important Events': 0, 'Events/Second': lambda s: ( (s['Important Events'] / (time.time() - s['Start Time']))), }) ... for event in events: ... # Collect stats if mystats.get('Enabled', False): mystats['Important Events'] += 1 To report statistics:: root.cpstats = cpstats.StatsPage() To format statistics reports:: See 'Reporting', above. """ import logging import os import sys import threading import time import cherrypy from cherrypy._json import json # ------------------------------- Statistics -------------------------------- # if not hasattr(logging, 'statistics'): logging.statistics = {} def extrapolate_statistics(scope): """Return an extrapolated copy of the given scope.""" c = {} for k, v in scope.copy().items(): if isinstance(v, dict): v = extrapolate_statistics(v) elif isinstance(v, (list, tuple)): v = [extrapolate_statistics(record) for record in v] elif hasattr(v, '__call__'): v = v(scope) c[k] = v return c # -------------------- CherryPy Applications Statistics --------------------- # appstats = logging.statistics.setdefault('CherryPy Applications', {}) appstats.update({ 'Enabled': True, 'Bytes Read/Request': lambda s: ( s['Total Requests'] and (s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0 ), 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s), 'Bytes Written/Request': lambda s: ( s['Total Requests'] and (s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0 ), 'Bytes Written/Second': lambda s: ( s['Total Bytes Written'] / s['Uptime'](s) ), 'Current Time': lambda s: time.time(), 'Current Requests': 0, 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s), 'Server Version': cherrypy.__version__, 'Start Time': time.time(), 'Total Bytes Read': 0, 'Total Bytes Written': 0, 'Total Requests': 0, 'Total Time': 0, 'Uptime': lambda s: time.time() - s['Start Time'], 'Requests': {}, }) def proc_time(s): return time.time() - s['Start Time'] class ByteCountWrapper(object): """Wraps a file-like object, counting the number of bytes read.""" def __init__(self, rfile): self.rfile = rfile self.bytes_read = 0 def read(self, size=-1): data = self.rfile.read(size) self.bytes_read += len(data) return data def readline(self, size=-1): data = self.rfile.readline(size) self.bytes_read += len(data) return data def readlines(self, sizehint=0): # Shamelessly stolen from StringIO total = 0 lines = [] line = self.readline() while line: lines.append(line) total += len(line) if 0 < sizehint <= total: break line = self.readline() return lines def close(self): self.rfile.close() def __iter__(self): return self def next(self): data = self.rfile.next() self.bytes_read += len(data) return data def average_uriset_time(s): return s['Count'] and (s['Sum'] / s['Count']) or 0 def _get_threading_ident(): if sys.version_info >= (3, 3): return threading.get_ident() return threading._get_ident() class StatsTool(cherrypy.Tool): """Record various information about the current request.""" def __init__(self): cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) def _setup(self): """Hook this tool into cherrypy.request. The standard CherryPy request object will automatically call this method when the tool is "turned on" in config. """ if appstats.get('Enabled', False): cherrypy.Tool._setup(self) self.record_start() def record_start(self): """Record the beginning of a request.""" request = cherrypy.serving.request if not hasattr(request.rfile, 'bytes_read'): request.rfile = ByteCountWrapper(request.rfile) request.body.fp = request.rfile r = request.remote appstats['Current Requests'] += 1 appstats['Total Requests'] += 1 appstats['Requests'][_get_threading_ident()] = { 'Bytes Read': None, 'Bytes Written': None, # Use a lambda so the ip gets updated by tools.proxy later 'Client': lambda s: '%s:%s' % (r.ip, r.port), 'End Time': None, 'Processing Time': proc_time, 'Request-Line': request.request_line, 'Response Status': None, 'Start Time': time.time(), } def record_stop( self, uriset=None, slow_queries=1.0, slow_queries_count=100, debug=False, **kwargs): """Record the end of a request.""" resp = cherrypy.serving.response w = appstats['Requests'][_get_threading_ident()] r = cherrypy.request.rfile.bytes_read w['Bytes Read'] = r appstats['Total Bytes Read'] += r if resp.stream: w['Bytes Written'] = 'chunked' else: cl = int(resp.headers.get('Content-Length', 0)) w['Bytes Written'] = cl appstats['Total Bytes Written'] += cl w['Response Status'] = \ getattr(resp, 'output_status', resp.status).decode() w['End Time'] = time.time() p = w['End Time'] - w['Start Time'] w['Processing Time'] = p appstats['Total Time'] += p appstats['Current Requests'] -= 1 if debug: cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') if uriset: rs = appstats.setdefault('URI Set Tracking', {}) r = rs.setdefault(uriset, { 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0, 'Avg': average_uriset_time}) if r['Min'] is None or p < r['Min']: r['Min'] = p if r['Max'] is None or p > r['Max']: r['Max'] = p r['Count'] += 1 r['Sum'] += p if slow_queries and p > slow_queries: sq = appstats.setdefault('Slow Queries', []) sq.append(w.copy()) if len(sq) > slow_queries_count: sq.pop(0) cherrypy.tools.cpstats = StatsTool() # ---------------------- CherryPy Statistics Reporting ---------------------- # thisdir = os.path.abspath(os.path.dirname(__file__)) missing = object() def locale_date(v): return time.strftime('%c', time.gmtime(v)) def iso_format(v): return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) def pause_resume(ns): def _pause_resume(enabled): pause_disabled = '' resume_disabled = '' if enabled: resume_disabled = 'disabled="disabled" ' else: pause_disabled = 'disabled="disabled" ' return """
""" % (ns, pause_disabled, ns, resume_disabled) return _pause_resume class StatsPage(object): formatting = { 'CherryPy Applications': { 'Enabled': pause_resume('CherryPy Applications'), 'Bytes Read/Request': '%.3f', 'Bytes Read/Second': '%.3f', 'Bytes Written/Request': '%.3f', 'Bytes Written/Second': '%.3f', 'Current Time': iso_format, 'Requests/Second': '%.3f', 'Start Time': iso_format, 'Total Time': '%.3f', 'Uptime': '%.3f', 'Slow Queries': { 'End Time': None, 'Processing Time': '%.3f', 'Start Time': iso_format, }, 'URI Set Tracking': { 'Avg': '%.3f', 'Max': '%.3f', 'Min': '%.3f', 'Sum': '%.3f', }, 'Requests': { 'Bytes Read': '%s', 'Bytes Written': '%s', 'End Time': None, 'Processing Time': '%.3f', 'Start Time': None, }, }, 'CherryPy WSGIServer': { 'Enabled': pause_resume('CherryPy WSGIServer'), 'Connections/second': '%.3f', 'Start time': iso_format, }, } @cherrypy.expose def index(self): # Transform the raw data into pretty output for HTML yield """ Statistics """ for title, scalars, collections in self.get_namespaces(): yield """

%s

""" % title for i, (key, value) in enumerate(scalars): colnum = i % 3 if colnum == 0: yield """ """ yield ( """ """ % vars() ) if colnum == 2: yield """ """ if colnum == 0: yield """ """ elif colnum == 1: yield """ """ yield """
%(key)s%(value)s
""" for subtitle, headers, subrows in collections: yield """

%s

""" % subtitle for key in headers: yield """ """ % key yield """ """ for subrow in subrows: yield """ """ for value in subrow: yield """ """ % value yield """ """ yield """
%s
%s
""" yield """ """ def get_namespaces(self): """Yield (title, scalars, collections) for each namespace.""" s = extrapolate_statistics(logging.statistics) for title, ns in sorted(s.items()): scalars = [] collections = [] ns_fmt = self.formatting.get(title, {}) for k, v in sorted(ns.items()): fmt = ns_fmt.get(k, {}) if isinstance(v, dict): headers, subrows = self.get_dict_collection(v, fmt) collections.append((k, ['ID'] + headers, subrows)) elif isinstance(v, (list, tuple)): headers, subrows = self.get_list_collection(v, fmt) collections.append((k, headers, subrows)) else: format = ns_fmt.get(k, missing) if format is None: # Don't output this column. continue if hasattr(format, '__call__'): v = format(v) elif format is not missing: v = format % v scalars.append((k, v)) yield title, scalars, collections def get_dict_collection(self, v, formatting): """Return ([headers], [rows]) for the given collection.""" # E.g., the 'Requests' dict. headers = [] vals = v.values() for record in vals: for k3 in record: format = formatting.get(k3, missing) if format is None: # Don't output this column. continue if k3 not in headers: headers.append(k3) headers.sort() subrows = [] for k2, record in sorted(v.items()): subrow = [k2] for k3 in headers: v3 = record.get(k3, '') format = formatting.get(k3, missing) if format is None: # Don't output this column. continue if hasattr(format, '__call__'): v3 = format(v3) elif format is not missing: v3 = format % v3 subrow.append(v3) subrows.append(subrow) return headers, subrows def get_list_collection(self, v, formatting): """Return ([headers], [subrows]) for the given collection.""" # E.g., the 'Slow Queries' list. headers = [] for record in v: for k3 in record: format = formatting.get(k3, missing) if format is None: # Don't output this column. continue if k3 not in headers: headers.append(k3) headers.sort() subrows = [] for record in v: subrow = [] for k3 in headers: v3 = record.get(k3, '') format = formatting.get(k3, missing) if format is None: # Don't output this column. continue if hasattr(format, '__call__'): v3 = format(v3) elif format is not missing: v3 = format % v3 subrow.append(v3) subrows.append(subrow) return headers, subrows if json is not None: @cherrypy.expose def data(self): s = extrapolate_statistics(logging.statistics) cherrypy.response.headers['Content-Type'] = 'application/json' return json.dumps(s, sort_keys=True, indent=4).encode('utf-8') @cherrypy.expose def pause(self, namespace): logging.statistics.get(namespace, {})['Enabled'] = False raise cherrypy.HTTPRedirect('./') pause.cp_config = {'tools.allow.on': True, 'tools.allow.methods': ['POST']} @cherrypy.expose def resume(self, namespace): logging.statistics.get(namespace, {})['Enabled'] = True raise cherrypy.HTTPRedirect('./') resume.cp_config = {'tools.allow.on': True, 'tools.allow.methods': ['POST']} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/cherrypy/lib/cptools.py0000644252176402575230000005575214424762003020720 0ustar00jaracoprimarygroup"""Functions for builtin CherryPy tools.""" import logging import re from hashlib import md5 import urllib.parse import cherrypy from cherrypy._cpcompat import text_or_bytes from cherrypy.lib import httputil as _httputil from cherrypy.lib import is_iterator # Conditional HTTP request support # def validate_etags(autotags=False, debug=False): """Validate the current ETag against If-Match, If-None-Match headers. If autotags is True, an ETag response-header value will be provided from an MD5 hash of the response body (unless some other code has already provided an ETag header). If False (the default), the ETag will not be automatic. WARNING: the autotags feature is not designed for URL's which allow methods other than GET. For example, if a POST to the same URL returns no content, the automatic ETag will be incorrect, breaking a fundamental use for entity tags in a possibly destructive fashion. Likewise, if you raise 304 Not Modified, the response body will be empty, the ETag hash will be incorrect, and your application will break. See :rfc:`2616` Section 14.24. """ response = cherrypy.serving.response # Guard against being run twice. if hasattr(response, 'ETag'): return status, reason, msg = _httputil.valid_status(response.status) etag = response.headers.get('ETag') # Automatic ETag generation. See warning in docstring. if etag: if debug: cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') elif not autotags: if debug: cherrypy.log('Autotags off', 'TOOLS.ETAGS') elif status != 200: if debug: cherrypy.log('Status not 200', 'TOOLS.ETAGS') else: etag = response.collapse_body() etag = '"%s"' % md5(etag).hexdigest() if debug: cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') response.headers['ETag'] = etag response.ETag = etag # "If the request would, without the If-Match header field, result in # anything other than a 2xx or 412 status, then the If-Match header # MUST be ignored." if debug: cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') if status >= 200 and status <= 299: request = cherrypy.serving.request conditions = request.headers.elements('If-Match') or [] conditions = [str(x) for x in conditions] if debug: cherrypy.log('If-Match conditions: %s' % repr(conditions), 'TOOLS.ETAGS') if conditions and not (conditions == ['*'] or etag in conditions): raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did ' 'not match %r' % (etag, conditions)) conditions = request.headers.elements('If-None-Match') or [] conditions = [str(x) for x in conditions] if debug: cherrypy.log('If-None-Match conditions: %s' % repr(conditions), 'TOOLS.ETAGS') if conditions == ['*'] or etag in conditions: if debug: cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS') if request.method in ('GET', 'HEAD'): raise cherrypy.HTTPRedirect([], 304) else: raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r ' 'matched %r' % (etag, conditions)) def validate_since(): """Validate the current Last-Modified against If-Modified-Since headers. If no code has set the Last-Modified response header, then no validation will be performed. """ response = cherrypy.serving.response lastmod = response.headers.get('Last-Modified') if lastmod: status, reason, msg = _httputil.valid_status(response.status) request = cherrypy.serving.request since = request.headers.get('If-Unmodified-Since') if since and since != lastmod: if (status >= 200 and status <= 299) or status == 412: raise cherrypy.HTTPError(412) since = request.headers.get('If-Modified-Since') if since and since == lastmod: if (status >= 200 and status <= 299) or status == 304: if request.method in ('GET', 'HEAD'): raise cherrypy.HTTPRedirect([], 304) else: raise cherrypy.HTTPError(412) # Tool code # def allow(methods=None, debug=False): """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). The given methods are case-insensitive, and may be in any order. If only one method is allowed, you may supply a single string; if more than one, supply a list of strings. Regardless of whether the current method is allowed or not, this also emits an 'Allow' response header, containing the given methods. """ if not isinstance(methods, (tuple, list)): methods = [methods] methods = [m.upper() for m in methods if m] if not methods: methods = ['GET', 'HEAD'] elif 'GET' in methods and 'HEAD' not in methods: methods.append('HEAD') cherrypy.response.headers['Allow'] = ', '.join(methods) if cherrypy.request.method not in methods: if debug: cherrypy.log('request.method %r not in methods %r' % (cherrypy.request.method, methods), 'TOOLS.ALLOW') raise cherrypy.HTTPError(405) else: if debug: cherrypy.log('request.method %r in methods %r' % (cherrypy.request.method, methods), 'TOOLS.ALLOW') def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', scheme='X-Forwarded-Proto', debug=False): """Change the base URL (scheme://host[:port][/path]). For running a CP server behind Apache, lighttpd, or other HTTP server. For Apache and lighttpd, you should leave the 'local' argument at the default value of 'X-Forwarded-Host'. For Squid, you probably want to set tools.proxy.local = 'Origin'. If you want the new request.base to include path info (not just the host), you must explicitly set base to the full base path, and ALSO set 'local' to '', so that the X-Forwarded-Host request header (which never includes path info) does not override it. Regardless, the value for 'base' MUST NOT end in a slash. cherrypy.request.remote.ip (the IP address of the client) will be rewritten if the header specified by the 'remote' arg is valid. By default, 'remote' is set to 'X-Forwarded-For'. If you do not want to rewrite remote.ip, set the 'remote' arg to an empty string. """ request = cherrypy.serving.request if scheme: s = request.headers.get(scheme, None) if debug: cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') if s == 'on' and 'ssl' in scheme.lower(): # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header scheme = 'https' else: # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' scheme = s if not scheme: scheme = request.base[:request.base.find('://')] if local: lbase = request.headers.get(local, None) if debug: cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') if lbase is not None: base = lbase.split(',')[0] if not base: default = urllib.parse.urlparse(request.base).netloc base = request.headers.get('Host', default) if base.find('://') == -1: # add http:// or https:// if needed base = scheme + '://' + base request.base = base if remote: xff = request.headers.get(remote) if debug: cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') if xff: if remote == 'X-Forwarded-For': # Grab the first IP in a comma-separated list. Ref #1268. xff = next(ip.strip() for ip in xff.split(',')) request.remote.ip = xff def ignore_headers(headers=('Range',), debug=False): """Delete request headers whose field names are included in 'headers'. This is a useful tool for working behind certain HTTP servers; for example, Apache duplicates the work that CP does for 'Range' headers, and will doubly-truncate the response. """ request = cherrypy.serving.request for name in headers: if name in request.headers: if debug: cherrypy.log('Ignoring request header %r' % name, 'TOOLS.IGNORE_HEADERS') del request.headers[name] def response_headers(headers=None, debug=False): """Set headers on the response.""" if debug: cherrypy.log('Setting response headers: %s' % repr(headers), 'TOOLS.RESPONSE_HEADERS') for name, value in (headers or []): cherrypy.serving.response.headers[name] = value response_headers.failsafe = True def referer(pattern, accept=True, accept_missing=False, error=403, message='Forbidden Referer header.', debug=False): """Raise HTTPError if Referer header does/does not match the given pattern. pattern A regular expression pattern to test against the Referer. accept If True, the Referer must match the pattern; if False, the Referer must NOT match the pattern. accept_missing If True, permit requests with no Referer header. error The HTTP error code to return to the client on failure. message A string to include in the response body on failure. """ try: ref = cherrypy.serving.request.headers['Referer'] match = bool(re.match(pattern, ref)) if debug: cherrypy.log('Referer %r matches %r' % (ref, pattern), 'TOOLS.REFERER') if accept == match: return except KeyError: if debug: cherrypy.log('No Referer header', 'TOOLS.REFERER') if accept_missing: return raise cherrypy.HTTPError(error, message) class SessionAuth(object): """Assert that the user is logged in.""" session_key = 'username' debug = False def check_username_and_password(self, username, password): pass def anonymous(self): """Provide a temporary user name for anonymous users.""" pass def on_login(self, username): pass def on_logout(self, username): pass def on_check(self, username): pass def login_screen(self, from_page='..', username='', error_msg='', **kwargs): return (str(""" Message: %(error_msg)s
Login:
Password:

""") % vars()).encode('utf-8') def do_login(self, username, password, from_page='..', **kwargs): """Login. May raise redirect, or return True if request handled.""" response = cherrypy.serving.response error_msg = self.check_username_and_password(username, password) if error_msg: body = self.login_screen(from_page, username, error_msg) response.body = body if 'Content-Length' in response.headers: # Delete Content-Length header so finalize() recalcs it. del response.headers['Content-Length'] return True else: cherrypy.serving.request.login = username cherrypy.session[self.session_key] = username self.on_login(username) raise cherrypy.HTTPRedirect(from_page or '/') def do_logout(self, from_page='..', **kwargs): """Logout. May raise redirect, or return True if request handled.""" sess = cherrypy.session username = sess.get(self.session_key) sess[self.session_key] = None if username: cherrypy.serving.request.login = None self.on_logout(username) raise cherrypy.HTTPRedirect(from_page) def do_check(self): """Assert username. Raise redirect, or return True if request handled. """ sess = cherrypy.session request = cherrypy.serving.request response = cherrypy.serving.response username = sess.get(self.session_key) if not username: sess[self.session_key] = username = self.anonymous() self._debug_message('No session[username], trying anonymous') if not username: url = cherrypy.url(qs=request.query_string) self._debug_message( 'No username, routing to login_screen with from_page %(url)r', locals(), ) response.body = self.login_screen(url) if 'Content-Length' in response.headers: # Delete Content-Length header so finalize() recalcs it. del response.headers['Content-Length'] return True self._debug_message('Setting request.login to %(username)r', locals()) request.login = username self.on_check(username) def _debug_message(self, template, context={}): if not self.debug: return cherrypy.log(template % context, 'TOOLS.SESSAUTH') def run(self): request = cherrypy.serving.request response = cherrypy.serving.response path = request.path_info if path.endswith('login_screen'): self._debug_message('routing %(path)r to login_screen', locals()) response.body = self.login_screen() return True elif path.endswith('do_login'): if request.method != 'POST': response.headers['Allow'] = 'POST' self._debug_message('do_login requires POST') raise cherrypy.HTTPError(405) self._debug_message('routing %(path)r to do_login', locals()) return self.do_login(**request.params) elif path.endswith('do_logout'): if request.method != 'POST': response.headers['Allow'] = 'POST' raise cherrypy.HTTPError(405) self._debug_message('routing %(path)r to do_logout', locals()) return self.do_logout(**request.params) else: self._debug_message('No special path, running do_check') return self.do_check() def session_auth(**kwargs): """Session authentication hook. Any attribute of the SessionAuth class may be overridden via a keyword arg to this function: """ + '\n '.join( '{!s}: {!s}'.format(k, type(getattr(SessionAuth, k)).__name__) for k in dir(SessionAuth) if not k.startswith('__') ) sa = SessionAuth() for k, v in kwargs.items(): setattr(sa, k, v) return sa.run() def log_traceback(severity=logging.ERROR, debug=False): """Write the last error's traceback to the cherrypy error log.""" cherrypy.log('', 'HTTP', severity=severity, traceback=True) def log_request_headers(debug=False): """Write request headers to the cherrypy error log.""" h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list] cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP') def log_hooks(debug=False): """Write request.hooks to the cherrypy error log.""" request = cherrypy.serving.request msg = [] # Sort by the standard points if possible. from cherrypy import _cprequest points = _cprequest.hookpoints for k in request.hooks.keys(): if k not in points: points.append(k) for k in points: msg.append(' %s:' % k) v = request.hooks.get(k, []) v.sort() for h in v: msg.append(' %r' % h) cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + ':\n' + '\n'.join(msg), 'HTTP') def redirect(url='', internal=True, debug=False): """Raise InternalRedirect or HTTPRedirect to the given url.""" if debug: cherrypy.log('Redirecting %sto: %s' % ({True: 'internal ', False: ''}[internal], url), 'TOOLS.REDIRECT') if internal: raise cherrypy.InternalRedirect(url) else: raise cherrypy.HTTPRedirect(url) def trailing_slash(missing=True, extra=False, status=None, debug=False): """Redirect if path_info has (missing|extra) trailing slash.""" request = cherrypy.serving.request pi = request.path_info if debug: cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % (request.is_index, missing, extra, pi), 'TOOLS.TRAILING_SLASH') if request.is_index is True: if missing: if not pi.endswith('/'): new_url = cherrypy.url(pi + '/', request.query_string) raise cherrypy.HTTPRedirect(new_url, status=status or 301) elif request.is_index is False: if extra: # If pi == '/', don't redirect to ''! if pi.endswith('/') and pi != '/': new_url = cherrypy.url(pi[:-1], request.query_string) raise cherrypy.HTTPRedirect(new_url, status=status or 301) def flatten(debug=False): """Wrap response.body in a generator that recursively iterates over body. This allows cherrypy.response.body to consist of 'nested generators'; that is, a set of generators that yield generators. """ def flattener(input): numchunks = 0 for x in input: if not is_iterator(x): numchunks += 1 yield x else: for y in flattener(x): numchunks += 1 yield y if debug: cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') response = cherrypy.serving.response response.body = flattener(response.body) def accept(media=None, debug=False): """Return the client's preferred media-type (from the given Content-Types). If 'media' is None (the default), no test will be performed. If 'media' is provided, it should be the Content-Type value (as a string) or values (as a list or tuple of strings) which the current resource can emit. The client's acceptable media ranges (as declared in the Accept request header) will be matched in order to these Content-Type values; the first such string is returned. That is, the return value will always be one of the strings provided in the 'media' arg (or None if 'media' is None). If no match is found, then HTTPError 406 (Not Acceptable) is raised. Note that most web browsers send */* as a (low-quality) acceptable media range, which should match any Content-Type. In addition, "...if no Accept header field is present, then it is assumed that the client accepts all media types." Matching types are checked in order of client preference first, and then in the order of the given 'media' values. Note that this function does not honor accept-params (other than "q"). """ if not media: return if isinstance(media, text_or_bytes): media = [media] request = cherrypy.serving.request # Parse the Accept request header, and try to match one # of the requested media-ranges (in order of preference). ranges = request.headers.elements('Accept') if not ranges: # Any media type is acceptable. if debug: cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') return media[0] else: # Note that 'ranges' is sorted in order of preference for element in ranges: if element.qvalue > 0: if element.value == '*/*': # Matches any type or subtype if debug: cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') return media[0] elif element.value.endswith('/*'): # Matches any subtype mtype = element.value[:-1] # Keep the slash for m in media: if m.startswith(mtype): if debug: cherrypy.log('Match due to %s' % element.value, 'TOOLS.ACCEPT') return m else: # Matches exact value if element.value in media: if debug: cherrypy.log('Match due to %s' % element.value, 'TOOLS.ACCEPT') return element.value # No suitable media-range found. ah = request.headers.get('Accept') if ah is None: msg = 'Your client did not send an Accept header.' else: msg = 'Your client sent this Accept header: %s.' % ah msg += (' But this resource only emits these media types: %s.' % ', '.join(media)) raise cherrypy.HTTPError(406, msg) class MonitoredHeaderMap(_httputil.HeaderMap): def transform_key(self, key): self.accessed_headers.add(key) return super(MonitoredHeaderMap, self).transform_key(key) def __init__(self): self.accessed_headers = set() super(MonitoredHeaderMap, self).__init__() def autovary(ignore=None, debug=False): """Auto-populate the Vary response header based on request.header access. """ request = cherrypy.serving.request req_h = request.headers request.headers = MonitoredHeaderMap() request.headers.update(req_h) if ignore is None: ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) def set_response_header(): resp_h = cherrypy.serving.response.headers v = set([e.value for e in resp_h.elements('Vary')]) if debug: cherrypy.log( 'Accessed headers: %s' % request.headers.accessed_headers, 'TOOLS.AUTOVARY') v = v.union(request.headers.accessed_headers) v = v.difference(ignore) v = list(v) v.sort() resp_h['Vary'] = ', '.join(v) request.hooks.attach('before_finalize', set_response_header, 95) def convert_params(exception=ValueError, error=400): """Convert request params based on function annotations. This function also processes errors that are subclasses of ``exception``. :param BaseException exception: Exception class to catch. :type exception: BaseException :param error: The HTTP status code to return to the client on failure. :type error: int """ request = cherrypy.serving.request types = request.handler.callable.__annotations__ with cherrypy.HTTPError.handle(exception, error): for key in set(types).intersection(request.params): request.params[key] = types[key](request.params[key]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/encoding.py0000644252176402575230000004122714307416152021014 0ustar00jaracoprimarygroupimport struct import time import io import cherrypy from cherrypy._cpcompat import text_or_bytes from cherrypy.lib import file_generator from cherrypy.lib import is_closable_iterator from cherrypy.lib import set_vary_header _COMPRESSION_LEVEL_FAST = 1 _COMPRESSION_LEVEL_BEST = 9 def decode(encoding=None, default_encoding='utf-8'): """Replace or extend the list of charsets used to decode a request entity. Either argument may be a single string or a list of strings. encoding If not None, restricts the set of charsets attempted while decoding a request entity to the given set (even if a different charset is given in the Content-Type request header). default_encoding Only in effect if the 'encoding' argument is not given. If given, the set of charsets attempted while decoding a request entity is *extended* with the given value(s). """ body = cherrypy.request.body if encoding is not None: if not isinstance(encoding, list): encoding = [encoding] body.attempt_charsets = encoding elif default_encoding: if not isinstance(default_encoding, list): default_encoding = [default_encoding] body.attempt_charsets = body.attempt_charsets + default_encoding class UTF8StreamEncoder: def __init__(self, iterator): self._iterator = iterator def __iter__(self): return self def next(self): return self.__next__() def __next__(self): res = next(self._iterator) if isinstance(res, str): res = res.encode('utf-8') return res def close(self): if is_closable_iterator(self._iterator): self._iterator.close() def __getattr__(self, attr): if attr.startswith('__'): raise AttributeError(self, attr) return getattr(self._iterator, attr) class ResponseEncoder: default_encoding = 'utf-8' failmsg = 'Response body could not be encoded with %r.' encoding = None errors = 'strict' text_only = True add_charset = True debug = False def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) self.attempted_charsets = set() request = cherrypy.serving.request if request.handler is not None: # Replace request.handler with self if self.debug: cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') self.oldhandler = request.handler request.handler = self def encode_stream(self, encoding): """Encode a streaming response body. Use a generator wrapper, and just pray it works as the stream is being written out. """ if encoding in self.attempted_charsets: return False self.attempted_charsets.add(encoding) def encoder(body): for chunk in body: if isinstance(chunk, str): chunk = chunk.encode(encoding, self.errors) yield chunk self.body = encoder(self.body) return True def encode_string(self, encoding): """Encode a buffered response body.""" if encoding in self.attempted_charsets: return False self.attempted_charsets.add(encoding) body = [] for chunk in self.body: if isinstance(chunk, str): try: chunk = chunk.encode(encoding, self.errors) except (LookupError, UnicodeError): return False body.append(chunk) self.body = body return True def find_acceptable_charset(self): request = cherrypy.serving.request response = cherrypy.serving.response if self.debug: cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') if response.stream: encoder = self.encode_stream else: encoder = self.encode_string if 'Content-Length' in response.headers: # Delete Content-Length header so finalize() recalcs it. # Encoded strings may be of different lengths from their # unicode equivalents, and even from each other. For example: # >>> t = u"\u7007\u3040" # >>> len(t) # 2 # >>> len(t.encode("UTF-8")) # 6 # >>> len(t.encode("utf7")) # 8 del response.headers['Content-Length'] # Parse the Accept-Charset request header, and try to provide one # of the requested charsets (in order of user preference). encs = request.headers.elements('Accept-Charset') charsets = [enc.value.lower() for enc in encs] if self.debug: cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') if self.encoding is not None: # If specified, force this encoding to be used, or fail. encoding = self.encoding.lower() if self.debug: cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE') if (not charsets) or '*' in charsets or encoding in charsets: if self.debug: cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE') if encoder(encoding): return encoding else: if not encs: if self.debug: cherrypy.log('Attempting default encoding %r' % self.default_encoding, 'TOOLS.ENCODE') # Any character-set is acceptable. if encoder(self.default_encoding): return self.default_encoding else: raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding) else: for element in encs: if element.qvalue > 0: if element.value == '*': # Matches any charset. Try our default. if self.debug: cherrypy.log('Attempting default encoding due ' 'to %r' % element, 'TOOLS.ENCODE') if encoder(self.default_encoding): return self.default_encoding else: encoding = element.value if self.debug: cherrypy.log('Attempting encoding %s (qvalue >' '0)' % element, 'TOOLS.ENCODE') if encoder(encoding): return encoding if '*' not in charsets: # If no "*" is present in an Accept-Charset field, then all # character sets not explicitly mentioned get a quality # value of 0, except for ISO-8859-1, which gets a quality # value of 1 if not explicitly mentioned. iso = 'iso-8859-1' if iso not in charsets: if self.debug: cherrypy.log('Attempting ISO-8859-1 encoding', 'TOOLS.ENCODE') if encoder(iso): return iso # No suitable encoding found. ac = request.headers.get('Accept-Charset') if ac is None: msg = 'Your client did not send an Accept-Charset header.' else: msg = 'Your client sent this Accept-Charset header: %s.' % ac _charsets = ', '.join(sorted(self.attempted_charsets)) msg += ' We tried these charsets: %s.' % (_charsets,) raise cherrypy.HTTPError(406, msg) def __call__(self, *args, **kwargs): response = cherrypy.serving.response self.body = self.oldhandler(*args, **kwargs) self.body = prepare_iter(self.body) ct = response.headers.elements('Content-Type') if self.debug: cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE') if ct and self.add_charset: ct = ct[0] if self.text_only: if ct.value.lower().startswith('text/'): if self.debug: cherrypy.log( 'Content-Type %s starts with "text/"' % ct, 'TOOLS.ENCODE') do_find = True else: if self.debug: cherrypy.log('Not finding because Content-Type %s ' 'does not start with "text/"' % ct, 'TOOLS.ENCODE') do_find = False else: if self.debug: cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE') do_find = True if do_find: # Set "charset=..." param on response Content-Type header ct.params['charset'] = self.find_acceptable_charset() if self.debug: cherrypy.log('Setting Content-Type %s' % ct, 'TOOLS.ENCODE') response.headers['Content-Type'] = str(ct) return self.body def prepare_iter(value): """ Ensure response body is iterable and resolves to False when empty. """ if isinstance(value, text_or_bytes): # strings get wrapped in a list because iterating over a single # item list is much faster than iterating over every character # in a long string. if value: value = [value] else: # [''] doesn't evaluate to False, so replace it with []. value = [] # Don't use isinstance here; io.IOBase which has an ABC takes # 1000 times as long as, say, isinstance(value, str) elif hasattr(value, 'read'): value = file_generator(value) elif value is None: value = [] return value # GZIP def compress(body, compress_level): """Compress 'body' at the given compress_level.""" import zlib # See https://tools.ietf.org/html/rfc1952 yield b'\x1f\x8b' # ID1 and ID2: gzip marker yield b'\x08' # CM: compression method yield b'\x00' # FLG: none set # MTIME: 4 bytes yield struct.pack(' 0 is present * The 'identity' value is given with a qvalue > 0. """ request = cherrypy.serving.request response = cherrypy.serving.response set_vary_header(response, 'Accept-Encoding') if not response.body: # Response body is empty (might be a 304 for instance) if debug: cherrypy.log('No response body', context='TOOLS.GZIP') return # If returning cached content (which should already have been gzipped), # don't re-zip. if getattr(request, 'cached', False): if debug: cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') return acceptable = request.headers.elements('Accept-Encoding') if not acceptable: # If no Accept-Encoding field is present in a request, # the server MAY assume that the client will accept any # content coding. In this case, if "identity" is one of # the available content-codings, then the server SHOULD use # the "identity" content-coding, unless it has additional # information that a different content-coding is meaningful # to the client. if debug: cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') return ct = response.headers.get('Content-Type', '').split(';')[0] for coding in acceptable: if coding.value == 'identity' and coding.qvalue != 0: if debug: cherrypy.log('Non-zero identity qvalue: %s' % coding, context='TOOLS.GZIP') return if coding.value in ('gzip', 'x-gzip'): if coding.qvalue == 0: if debug: cherrypy.log('Zero gzip qvalue: %s' % coding, context='TOOLS.GZIP') return if ct not in mime_types: # If the list of provided mime-types contains tokens # such as 'text/*' or 'application/*+xml', # we go through them and find the most appropriate one # based on the given content-type. # The pattern matching is only caring about the most # common cases, as stated above, and doesn't support # for extra parameters. found = False if '/' in ct: ct_media_type, ct_sub_type = ct.split('/') for mime_type in mime_types: if '/' in mime_type: media_type, sub_type = mime_type.split('/') if ct_media_type == media_type: if sub_type == '*': found = True break elif '+' in sub_type and '+' in ct_sub_type: ct_left, ct_right = ct_sub_type.split('+') left, right = sub_type.split('+') if left == '*' and ct_right == right: found = True break if not found: if debug: cherrypy.log('Content-Type %s not in mime_types %r' % (ct, mime_types), context='TOOLS.GZIP') return if debug: cherrypy.log('Gzipping', context='TOOLS.GZIP') # Return a generator that compresses the page response.headers['Content-Encoding'] = 'gzip' response.body = compress(response.body, compress_level) if 'Content-Length' in response.headers: # Delete Content-Length header so finalize() recalcs it. del response.headers['Content-Length'] return if debug: cherrypy.log('No acceptable encoding found.', context='GZIP') cherrypy.HTTPError(406, 'identity, gzip').set_response() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/gctools.py0000644252176402575230000001626214307416152020701 0ustar00jaracoprimarygroupimport gc import inspect import sys import time try: import objgraph except ImportError: objgraph = None import cherrypy from cherrypy import _cprequest, _cpwsgi from cherrypy.process.plugins import SimplePlugin class ReferrerTree(object): """An object which gathers all referrers of an object to a given depth.""" peek_length = 40 def __init__(self, ignore=None, maxdepth=2, maxparents=10): self.ignore = ignore or [] self.ignore.append(inspect.currentframe().f_back) self.maxdepth = maxdepth self.maxparents = maxparents def ascend(self, obj, depth=1): """Return a nested list containing referrers of the given object.""" depth += 1 parents = [] # Gather all referrers in one step to minimize # cascading references due to repr() logic. refs = gc.get_referrers(obj) self.ignore.append(refs) if len(refs) > self.maxparents: return [('[%s referrers]' % len(refs), [])] try: ascendcode = self.ascend.__code__ except AttributeError: ascendcode = self.ascend.im_func.func_code for parent in refs: if inspect.isframe(parent) and parent.f_code is ascendcode: continue if parent in self.ignore: continue if depth <= self.maxdepth: parents.append((parent, self.ascend(parent, depth))) else: parents.append((parent, [])) return parents def peek(self, s): """Return s, restricted to a sane length.""" if len(s) > (self.peek_length + 3): half = self.peek_length // 2 return s[:half] + '...' + s[-half:] else: return s def _format(self, obj, descend=True): """Return a string representation of a single object.""" if inspect.isframe(obj): filename, lineno, func, context, index = inspect.getframeinfo(obj) return "" % func if not descend: return self.peek(repr(obj)) if isinstance(obj, dict): return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False), self._format(v, descend=False)) for k, v in obj.items()]) + '}' elif isinstance(obj, list): return '[' + ', '.join([self._format(item, descend=False) for item in obj]) + ']' elif isinstance(obj, tuple): return '(' + ', '.join([self._format(item, descend=False) for item in obj]) + ')' r = self.peek(repr(obj)) if isinstance(obj, (str, int, float)): return r return '%s: %s' % (type(obj), r) def format(self, tree): """Return a list of string reprs from a nested list of referrers.""" output = [] def ascend(branch, depth=1): for parent, grandparents in branch: output.append((' ' * depth) + self._format(parent)) if grandparents: ascend(grandparents, depth + 1) ascend(tree) return output def get_instances(cls): return [x for x in gc.get_objects() if isinstance(x, cls)] class RequestCounter(SimplePlugin): def start(self): self.count = 0 def before_request(self): self.count += 1 def after_request(self): self.count -= 1 request_counter = RequestCounter(cherrypy.engine) request_counter.subscribe() def get_context(obj): if isinstance(obj, _cprequest.Request): return 'path=%s;stage=%s' % (obj.path_info, obj.stage) elif isinstance(obj, _cprequest.Response): return 'status=%s' % obj.status elif isinstance(obj, _cpwsgi.AppResponse): return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '') elif hasattr(obj, 'tb_lineno'): return 'tb_lineno=%s' % obj.tb_lineno return '' class GCRoot(object): """A CherryPy page handler for testing reference leaks.""" classes = [ (_cprequest.Request, 2, 2, 'Should be 1 in this request thread and 1 in the main thread.'), (_cprequest.Response, 2, 2, 'Should be 1 in this request thread and 1 in the main thread.'), (_cpwsgi.AppResponse, 1, 1, 'Should be 1 in this request thread only.'), ] @cherrypy.expose def index(self): return 'Hello, world!' @cherrypy.expose def stats(self): output = ['Statistics:'] for trial in range(10): if request_counter.count > 0: break time.sleep(0.5) else: output.append('\nNot all requests closed properly.') # gc_collect isn't perfectly synchronous, because it may # break reference cycles that then take time to fully # finalize. Call it thrice and hope for the best. gc.collect() gc.collect() unreachable = gc.collect() if unreachable: if objgraph is not None: final = objgraph.by_type('Nondestructible') if final: objgraph.show_backrefs(final, filename='finalizers.png') trash = {} for x in gc.garbage: trash[type(x)] = trash.get(type(x), 0) + 1 if trash: output.insert(0, '\n%s unreachable objects:' % unreachable) trash = [(v, k) for k, v in trash.items()] trash.sort() for pair in trash: output.append(' ' + repr(pair)) # Check declared classes to verify uncollected instances. # These don't have to be part of a cycle; they can be # any objects that have unanticipated referrers that keep # them from being collected. allobjs = {} for cls, minobj, maxobj, msg in self.classes: allobjs[cls] = get_instances(cls) for cls, minobj, maxobj, msg in self.classes: objs = allobjs[cls] lenobj = len(objs) if lenobj < minobj or lenobj > maxobj: if minobj == maxobj: output.append( '\nExpected %s %r references, got %s.' % (minobj, cls, lenobj)) else: output.append( '\nExpected %s to %s %r references, got %s.' % (minobj, maxobj, cls, lenobj)) for obj in objs: if objgraph is not None: ig = [id(objs), id(inspect.currentframe())] fname = 'graph_%s_%s.png' % (cls.__name__, id(obj)) objgraph.show_backrefs( obj, extra_ignore=ig, max_depth=4, too_many=20, filename=fname, extra_info=get_context) output.append('\nReferrers for %s (refcount=%s):' % (repr(obj), sys.getrefcount(obj))) t = ReferrerTree(ignore=[objs], maxdepth=3) tree = t.ascend(obj) output.extend(t.format(tree)) return '\n'.join(output) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/httputil.py0000644252176402575230000004312714307416152021104 0ustar00jaracoprimarygroup"""HTTP library functions. This module contains functions for building an HTTP application framework: any one, not just one whose name starts with "Ch". ;) If you reference any modules from some popular framework inside *this* module, FuManChu will personally hang you up by your thumbs and submit you to a public caning. """ import functools import email.utils import re import builtins from binascii import b2a_base64 from cgi import parse_header from email.header import decode_header from http.server import BaseHTTPRequestHandler from urllib.parse import unquote_plus import jaraco.collections import cherrypy from cherrypy._cpcompat import ntob, ntou response_codes = BaseHTTPRequestHandler.responses.copy() # From https://github.com/cherrypy/cherrypy/issues/361 response_codes[500] = ('Internal Server Error', 'The server encountered an unexpected condition ' 'which prevented it from fulfilling the request.') response_codes[503] = ('Service Unavailable', 'The server is currently unable to handle the ' 'request due to a temporary overloading or ' 'maintenance of the server.') HTTPDate = functools.partial(email.utils.formatdate, usegmt=True) def urljoin(*atoms): r"""Return the given path \*atoms, joined into a single URL. This will correctly join a SCRIPT_NAME and PATH_INFO into the original URL, even if either atom is blank. """ url = '/'.join([x for x in atoms if x]) while '//' in url: url = url.replace('//', '/') # Special-case the final url of "", and return "/" instead. return url or '/' def urljoin_bytes(*atoms): """Return the given path `*atoms`, joined into a single URL. This will correctly join a SCRIPT_NAME and PATH_INFO into the original URL, even if either atom is blank. """ url = b'/'.join([x for x in atoms if x]) while b'//' in url: url = url.replace(b'//', b'/') # Special-case the final url of "", and return "/" instead. return url or b'/' def protocol_from_http(protocol_str): """Return a protocol tuple from the given 'HTTP/x.y' string.""" return int(protocol_str[5]), int(protocol_str[7]) def get_ranges(headervalue, content_length): """Return a list of (start, stop) indices from a Range header, or None. Each (start, stop) tuple will be composed of two ints, which are suitable for use in a slicing operation. That is, the header "Range: bytes=3-6", if applied against a Python string, is requesting resource[3:7]. This function will return the list [(3, 7)]. If this function returns an empty list, you should return HTTP 416. """ if not headervalue: return None result = [] bytesunit, byteranges = headervalue.split('=', 1) for brange in byteranges.split(','): start, stop = [x.strip() for x in brange.split('-', 1)] if start: if not stop: stop = content_length - 1 start, stop = int(start), int(stop) if start >= content_length: # From rfc 2616 sec 14.16: # "If the server receives a request (other than one # including an If-Range request-header field) with an # unsatisfiable Range request-header field (that is, # all of whose byte-range-spec values have a first-byte-pos # value greater than the current length of the selected # resource), it SHOULD return a response code of 416 # (Requested range not satisfiable)." continue if stop < start: # From rfc 2616 sec 14.16: # "If the server ignores a byte-range-spec because it # is syntactically invalid, the server SHOULD treat # the request as if the invalid Range header field # did not exist. (Normally, this means return a 200 # response containing the full entity)." return None result.append((start, stop + 1)) else: if not stop: # See rfc quote above. return None # Negative subscript (last N bytes) # # RFC 2616 Section 14.35.1: # If the entity is shorter than the specified suffix-length, # the entire entity-body is used. if int(stop) > content_length: result.append((0, content_length)) else: result.append((content_length - int(stop), content_length)) return result class HeaderElement(object): """An element (with parameters) from an HTTP header's element list.""" def __init__(self, value, params=None): self.value = value if params is None: params = {} self.params = params def __cmp__(self, other): return builtins.cmp(self.value, other.value) def __lt__(self, other): return self.value < other.value def __str__(self): p = [';%s=%s' % (k, v) for k, v in self.params.items()] return str('%s%s' % (self.value, ''.join(p))) def __bytes__(self): return ntob(self.__str__()) def __unicode__(self): return ntou(self.__str__()) @staticmethod def parse(elementstr): """Transform 'token;key=val' to ('token', {'key': 'val'}).""" initial_value, params = parse_header(elementstr) return initial_value, params @classmethod def from_str(cls, elementstr): """Construct an instance from a string of the form 'token;key=val'.""" ival, params = cls.parse(elementstr) return cls(ival, params) q_separator = re.compile(r'; *q *=') class AcceptElement(HeaderElement): """An element (with parameters) from an Accept* header's element list. AcceptElement objects are comparable; the more-preferred object will be "less than" the less-preferred object. They are also therefore sortable; if you sort a list of AcceptElement objects, they will be listed in priority order; the most preferred value will be first. Yes, it should have been the other way around, but it's too late to fix now. """ @classmethod def from_str(cls, elementstr): qvalue = None # The first "q" parameter (if any) separates the initial # media-range parameter(s) (if any) from the accept-params. atoms = q_separator.split(elementstr, 1) media_range = atoms.pop(0).strip() if atoms: # The qvalue for an Accept header can have extensions. The other # headers cannot, but it's easier to parse them as if they did. qvalue = HeaderElement.from_str(atoms[0].strip()) media_type, params = cls.parse(media_range) if qvalue is not None: params['q'] = qvalue return cls(media_type, params) @property def qvalue(self): 'The qvalue, or priority, of this value.' val = self.params.get('q', '1') if isinstance(val, HeaderElement): val = val.value try: return float(val) except ValueError as val_err: """Fail client requests with invalid quality value. Ref: https://github.com/cherrypy/cherrypy/issues/1370 """ raise cherrypy.HTTPError( 400, 'Malformed HTTP header: `{}`'. format(str(self)), ) from val_err def __cmp__(self, other): diff = builtins.cmp(self.qvalue, other.qvalue) if diff == 0: diff = builtins.cmp(str(self), str(other)) return diff def __lt__(self, other): if self.qvalue == other.qvalue: return str(self) < str(other) else: return self.qvalue < other.qvalue RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') def header_elements(fieldname, fieldvalue): """Return a sorted HeaderElement list from a comma-separated header string. """ if not fieldvalue: return [] result = [] for element in RE_HEADER_SPLIT.split(fieldvalue): if fieldname.startswith('Accept') or fieldname == 'TE': hv = AcceptElement.from_str(element) else: hv = HeaderElement.from_str(element) result.append(hv) return list(reversed(sorted(result))) def decode_TEXT(value): r""" Decode :rfc:`2047` TEXT >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1') True """ atoms = decode_header(value) decodedvalue = '' for atom, charset in atoms: if charset is not None: atom = atom.decode(charset) decodedvalue += atom return decodedvalue def decode_TEXT_maybe(value): """ Decode the text but only if '=?' appears in it. """ return decode_TEXT(value) if '=?' in value else value def valid_status(status): """Return legal HTTP status Code, Reason-phrase and Message. The status arg must be an int, a str that begins with an int or the constant from ``http.client`` stdlib module. If status has no reason-phrase is supplied, a default reason- phrase will be provided. >>> import http.client >>> from http.server import BaseHTTPRequestHandler >>> valid_status(http.client.ACCEPTED) == ( ... int(http.client.ACCEPTED), ... ) + BaseHTTPRequestHandler.responses[http.client.ACCEPTED] True """ if not status: status = 200 code, reason = status, None if isinstance(status, str): code, _, reason = status.partition(' ') reason = reason.strip() or None try: code = int(code) except (TypeError, ValueError): raise ValueError('Illegal response status from server ' '(%s is non-numeric).' % repr(code)) if code < 100 or code > 599: raise ValueError('Illegal response status from server ' '(%s is out of range).' % repr(code)) if code not in response_codes: # code is unknown but not illegal default_reason, message = '', '' else: default_reason, message = response_codes[code] if reason is None: reason = default_reason return code, reason, message # NOTE: the parse_qs functions that follow are modified version of those # in the python3.0 source - we need to pass through an encoding to the unquote # method, but the default parse_qs function doesn't allow us to. These do. def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): """Parse a query given as a string argument. Arguments: qs: URL-encoded query string to be parsed keep_blank_values: flag indicating whether blank values in URL encoded queries should be treated as blank strings. A true value indicates that blanks should be retained as blank strings. The default false value indicates that blank values are to be ignored and treated as if they were not included. strict_parsing: flag indicating what to do with parsing errors. If false (the default), errors are silently ignored. If true, errors raise a ValueError exception. Returns a dict, as G-d intended. """ pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] d = {} for name_value in pairs: if not name_value and not strict_parsing: continue nv = name_value.split('=', 1) if len(nv) != 2: if strict_parsing: raise ValueError('bad query field: %r' % (name_value,)) # Handle case of a control-name with no equal sign if keep_blank_values: nv.append('') else: continue if len(nv[1]) or keep_blank_values: name = unquote_plus(nv[0], encoding, errors='strict') value = unquote_plus(nv[1], encoding, errors='strict') if name in d: if not isinstance(d[name], list): d[name] = [d[name]] d[name].append(value) else: d[name] = value return d image_map_pattern = re.compile(r'[0-9]+,[0-9]+') def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): """Build a params dictionary from a query_string. Duplicate key/value pairs in the provided query_string will be returned as {'key': [val1, val2, ...]}. Single key/values will be returned as strings: {'key': 'value'}. """ if image_map_pattern.match(query_string): # Server-side image map. Map the coords to 'x' and 'y' # (like CGI::Request does). pm = query_string.split(',') pm = {'x': int(pm[0]), 'y': int(pm[1])} else: pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) return pm class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict): """A case-insensitive dict subclass. Each key is changed on entry to title case. """ @staticmethod def transform_key(key): if key is None: # TODO(#1830): why? return 'None' return key.title() # TEXT = # # A CRLF is allowed in the definition of TEXT only as part of a header # field continuation. It is expected that the folding LWS will be # replaced with a single SP before interpretation of the TEXT value." if str == bytes: header_translate_table = ''.join([chr(i) for i in range(256)]) header_translate_deletechars = ''.join( [chr(i) for i in range(32)]) + chr(127) else: header_translate_table = None header_translate_deletechars = bytes(range(32)) + bytes([127]) class HeaderMap(CaseInsensitiveDict): """A dict subclass for HTTP request and response headers. Each key is changed on entry to str(key).title(). This allows headers to be case-insensitive and avoid duplicates. Values are header values (decoded according to :rfc:`2047` if necessary). """ protocol = (1, 1) encodings = ['ISO-8859-1'] # Someday, when http-bis is done, this will probably get dropped # since few servers, clients, or intermediaries do it. But until then, # we're going to obey the spec as is. # "Words of *TEXT MAY contain characters from character sets other than # ISO-8859-1 only when encoded according to the rules of RFC 2047." use_rfc_2047 = True def elements(self, key): """Return a sorted list of HeaderElements for the given header.""" return header_elements(self.transform_key(key), self.get(key)) def values(self, key): """Return a sorted list of HeaderElement.value for the given header.""" return [e.value for e in self.elements(key)] def output(self): """Transform self into a list of (name, value) tuples.""" return list(self.encode_header_items(self.items())) @classmethod def encode_header_items(cls, header_items): """ Prepare the sequence of name, value tuples into a form suitable for transmitting on the wire for HTTP. """ for k, v in header_items: if not isinstance(v, str) and not isinstance(v, bytes): v = str(v) yield tuple(map(cls.encode_header_item, (k, v))) @classmethod def encode_header_item(cls, item): if isinstance(item, str): item = cls.encode(item) # See header_translate_* constants above. # Replace only if you really know what you're doing. return item.translate( header_translate_table, header_translate_deletechars) @classmethod def encode(cls, v): """Return the given header name or value, encoded for HTTP output.""" for enc in cls.encodings: try: return v.encode(enc) except UnicodeEncodeError: continue if cls.protocol == (1, 1) and cls.use_rfc_2047: # Encode RFC-2047 TEXT # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). # We do our own here instead of using the email module # because we never want to fold lines--folding has # been deprecated by the HTTP working group. v = b2a_base64(v.encode('utf-8')) return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=') raise ValueError('Could not encode header part %r using ' 'any of the encodings %r.' % (v, cls.encodings)) class Host(object): """An internet address. name Should be the client's host name. If not available (because no DNS lookup is performed), the IP address should be used instead. """ ip = '0.0.0.0' port = 80 name = 'unknown.tld' def __init__(self, ip, port, name=None): self.ip = ip self.port = port if name is None: name = ip self.name = name def __repr__(self): return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name) class SanitizedHost(str): r""" Wraps a raw host header received from the network in a sanitized version that elides dangerous characters. >>> SanitizedHost('foo\nbar') 'foobar' >>> SanitizedHost('foo\nbar').raw 'foo\nbar' A SanitizedInstance is only returned if sanitization was performed. >>> isinstance(SanitizedHost('foobar'), SanitizedHost) False """ dangerous = re.compile(r'[\n\r]') def __new__(cls, raw): sanitized = cls._sanitize(raw) if sanitized == raw: return raw instance = super().__new__(cls, sanitized) instance.raw = raw return instance @classmethod def _sanitize(cls, raw): return cls.dangerous.sub('', raw) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/jsontools.py0000644252176402575230000000707114307416152021257 0ustar00jaracoprimarygroupimport cherrypy from cherrypy import _json as json from cherrypy._cpcompat import text_or_bytes, ntou def json_processor(entity): """Read application/json data into request.json.""" if not entity.headers.get(ntou('Content-Length'), ntou('')): raise cherrypy.HTTPError(411) body = entity.fp.read() with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'): cherrypy.serving.request.json = json.decode(body.decode('utf-8')) def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], force=True, debug=False, processor=json_processor): """Add a processor to parse JSON request entities: The default processor places the parsed data into request.json. Incoming request entities which match the given content_type(s) will be deserialized from JSON to the Python equivalent, and the result stored at cherrypy.request.json. The 'content_type' argument may be a Content-Type string or a list of allowable Content-Type strings. If the 'force' argument is True (the default), then entities of other content types will not be allowed; "415 Unsupported Media Type" is raised instead. Supply your own processor to use a custom decoder, or to handle the parsed data differently. The processor can be configured via tools.json_in.processor or via the decorator method. Note that the deserializer requires the client send a Content-Length request header, or it will raise "411 Length Required". If for any other reason the request entity cannot be deserialized from JSON, it will raise "400 Bad Request: Invalid JSON document". """ request = cherrypy.serving.request if isinstance(content_type, text_or_bytes): content_type = [content_type] if force: if debug: cherrypy.log('Removing body processors %s' % repr(request.body.processors.keys()), 'TOOLS.JSON_IN') request.body.processors.clear() request.body.default_proc = cherrypy.HTTPError( 415, 'Expected an entity of content type %s' % ', '.join(content_type)) for ct in content_type: if debug: cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') request.body.processors[ct] = processor def json_handler(*args, **kwargs): value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) return json.encode(value) def json_out(content_type='application/json', debug=False, handler=json_handler): """Wrap request.handler to serialize its output to JSON. Sets Content-Type. If the given content_type is None, the Content-Type response header is not set. Provide your own handler to use a custom encoder. For example cherrypy.config['tools.json_out.handler'] = , or @json_out(handler=function). """ request = cherrypy.serving.request # request.handler may be set to None by e.g. the caching tool # to signal to all components that a response body has already # been attached, in which case we don't need to wrap anything. if request.handler is None: return if debug: cherrypy.log('Replacing %s with JSON handler' % request.handler, 'TOOLS.JSON_OUT') request._json_inner_handler = request.handler request.handler = handler if content_type is not None: if debug: cherrypy.log('Setting Content-Type to %s' % content_type, 'TOOLS.JSON_OUT') cherrypy.serving.response.headers['Content-Type'] = content_type ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/locking.py0000644252176402575230000000231014307416152020642 0ustar00jaracoprimarygroupimport datetime class NeverExpires(object): def expired(self): return False class Timer(object): """ A simple timer that will indicate when an expiration time has passed. """ def __init__(self, expiration): 'Create a timer that expires at `expiration` (UTC datetime)' self.expiration = expiration @classmethod def after(cls, elapsed): """ Return a timer that will expire after `elapsed` passes. """ return cls(datetime.datetime.utcnow() + elapsed) def expired(self): return datetime.datetime.utcnow() >= self.expiration class LockTimeout(Exception): 'An exception when a lock could not be acquired before a timeout period' class LockChecker(object): """ Keep track of the time and detect if a timeout has expired """ def __init__(self, session_id, timeout): self.session_id = session_id if timeout: self.timer = Timer.after(timeout) else: self.timer = NeverExpires() def expired(self): if self.timer.expired(): raise LockTimeout( 'Timeout acquiring lock for %(session_id)s' % vars(self)) return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/cherrypy/lib/profiler.py0000644252176402575230000001463414424762003021051 0ustar00jaracoprimarygroup"""Profiler tools for CherryPy. CherryPy users ============== You can profile any of your pages as follows:: from cherrypy.lib import profiler class Root: p = profiler.Profiler("/path/to/profile/dir") @cherrypy.expose def index(self): self.p.run(self._index) def _index(self): return "Hello, world!" cherrypy.tree.mount(Root()) You can also turn on profiling for all requests using the ``make_app`` function as WSGI middleware. CherryPy developers =================== This module can be used whenever you make changes to CherryPy, to get a quick sanity-check on overall CP performance. Use the ``--profile`` flag when running the test suite. Then, use the ``serve()`` function to browse the results in a web browser. If you run this module from the command line, it will call ``serve()`` for you. """ import io import os import os.path import sys import warnings import cherrypy try: import profile import pstats def new_func_strip_path(func_name): """Add ``__init__`` modules' parents. This makes the profiler output more readable. """ filename, line, name = func_name if filename.endswith('__init__.py'): return ( os.path.basename(filename[:-12]) + filename[-12:], line, name, ) return os.path.basename(filename), line, name pstats.func_strip_path = new_func_strip_path except ImportError: profile = None pstats = None _count = 0 class Profiler(object): def __init__(self, path=None): if not path: path = os.path.join(os.path.dirname(__file__), 'profile') self.path = path if not os.path.exists(path): os.makedirs(path) def run(self, func, *args, **params): """Dump profile data into self.path.""" global _count c = _count = _count + 1 path = os.path.join(self.path, 'cp_%04d.prof' % c) prof = profile.Profile() result = prof.runcall(func, *args, **params) prof.dump_stats(path) return result def statfiles(self): """:rtype: list of available profiles. """ return [f for f in os.listdir(self.path) if f.startswith('cp_') and f.endswith('.prof')] def stats(self, filename, sortby='cumulative'): """:rtype stats(index): output of print_stats() for the given profile. """ sio = io.StringIO() if sys.version_info >= (2, 5): s = pstats.Stats(os.path.join(self.path, filename), stream=sio) s.strip_dirs() s.sort_stats(sortby) s.print_stats() else: # pstats.Stats before Python 2.5 didn't take a 'stream' arg, # but just printed to stdout. So re-route stdout. s = pstats.Stats(os.path.join(self.path, filename)) s.strip_dirs() s.sort_stats(sortby) oldout = sys.stdout try: sys.stdout = sio s.print_stats() finally: sys.stdout = oldout response = sio.getvalue() sio.close() return response @cherrypy.expose def index(self): return """ CherryPy profile data """ @cherrypy.expose def menu(self): yield '

Profiling runs

' yield '

Click on one of the runs below to see profiling data.

' runs = self.statfiles() runs.sort() for i in runs: yield "%s
" % ( i, i) @cherrypy.expose def report(self, filename): cherrypy.response.headers['Content-Type'] = 'text/plain' return self.stats(filename) class ProfileAggregator(Profiler): def __init__(self, path=None): Profiler.__init__(self, path) global _count self.count = _count = _count + 1 self.profiler = profile.Profile() def run(self, func, *args, **params): path = os.path.join(self.path, 'cp_%04d.prof' % self.count) result = self.profiler.runcall(func, *args, **params) self.profiler.dump_stats(path) return result class make_app: def __init__(self, nextapp, path=None, aggregate=False): """Make a WSGI middleware app which wraps 'nextapp' with profiling. nextapp the WSGI application to wrap, usually an instance of cherrypy.Application. path where to dump the profiling output. aggregate if True, profile data for all HTTP requests will go in a single file. If False (the default), each HTTP request will dump its profile data into a separate file. """ if profile is None or pstats is None: msg = ('Your installation of Python does not have a profile ' "module. If you're on Debian, try " '`sudo apt-get install python-profiler`. ' 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' 'for details.') warnings.warn(msg) self.nextapp = nextapp self.aggregate = aggregate if aggregate: self.profiler = ProfileAggregator(path) else: self.profiler = Profiler(path) def __call__(self, environ, start_response): def gather(): result = [] for line in self.nextapp(environ, start_response): result.append(line) return result return self.profiler.run(gather) def serve(path=None, port=8080): if profile is None or pstats is None: msg = ('Your installation of Python does not have a profile module. ' "If you're on Debian, try " '`sudo apt-get install python-profiler`. ' 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' 'for details.') warnings.warn(msg) cherrypy.config.update({'server.socket_port': int(port), 'server.thread_pool': 10, 'environment': 'production', }) cherrypy.quickstart(Profiler(path)) if __name__ == '__main__': serve(*tuple(sys.argv[1:])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/cherrypy/lib/reprconf.py0000644252176402575230000003005114424762003021034 0ustar00jaracoprimarygroup"""Generic configuration system using unrepr. Configuration data may be supplied as a Python dictionary, as a filename, or as an open file object. When you supply a filename or file, Python's builtin ConfigParser is used (with some extensions). Namespaces ---------- Configuration keys are separated into namespaces by the first "." in the key. The only key that cannot exist in a namespace is the "environment" entry. This special entry 'imports' other config entries from a template stored in the Config.environments dict. You can define your own namespaces to be called when new config is merged by adding a named handler to Config.namespaces. The name can be any string, and the handler must be either a callable or a context manager. """ import builtins import configparser import operator import sys from cherrypy._cpcompat import text_or_bytes class NamespaceSet(dict): """A dict of config namespace names and handlers. Each config entry should begin with a namespace name; the corresponding namespace handler will be called once for each config entry in that namespace, and will be passed two arguments: the config key (with the namespace removed) and the config value. Namespace handlers may be any Python callable; they may also be context managers, in which case their __enter__ method should return a callable to be used as the handler. See cherrypy.tools (the Toolbox class) for an example. """ def __call__(self, config): """Iterate through config and pass it to each namespace handler. config A flat dict, where keys use dots to separate namespaces, and values are arbitrary. The first name in each config key is used to look up the corresponding namespace handler. For example, a config entry of {'tools.gzip.on': v} will call the 'tools' namespace handler with the args: ('gzip.on', v) """ # Separate the given config into namespaces ns_confs = {} for k in config: if '.' in k: ns, name = k.split('.', 1) bucket = ns_confs.setdefault(ns, {}) bucket[name] = config[k] # I chose __enter__ and __exit__ so someday this could be # rewritten using 'with' statement: # for ns, handler in self.items(): # with handler as callable: # for k, v in ns_confs.get(ns, {}).items(): # callable(k, v) for ns, handler in self.items(): exit = getattr(handler, '__exit__', None) if exit: callable = handler.__enter__() no_exc = True try: try: for k, v in ns_confs.get(ns, {}).items(): callable(k, v) except Exception: # The exceptional case is handled here no_exc = False if exit is None: raise if not exit(*sys.exc_info()): raise # The exception is swallowed if exit() returns true finally: # The normal and non-local-goto cases are handled here if no_exc and exit: exit(None, None, None) else: for k, v in ns_confs.get(ns, {}).items(): handler(k, v) def __repr__(self): return '%s.%s(%s)' % (self.__module__, self.__class__.__name__, dict.__repr__(self)) def __copy__(self): newobj = self.__class__() newobj.update(self) return newobj copy = __copy__ class Config(dict): """A dict-like set of configuration data, with defaults and namespaces. May take a file, filename, or dict. """ defaults = {} environments = {} namespaces = NamespaceSet() def __init__(self, file=None, **kwargs): self.reset() if file is not None: self.update(file) if kwargs: self.update(kwargs) def reset(self): """Reset self to default values.""" self.clear() dict.update(self, self.defaults) def update(self, config): """Update self from a dict, file, or filename.""" self._apply(Parser.load(config)) def _apply(self, config): """Update self from a dict.""" which_env = config.get('environment') if which_env: env = self.environments[which_env] for k in env: if k not in config: config[k] = env[k] dict.update(self, config) self.namespaces(config) def __setitem__(self, k, v): dict.__setitem__(self, k, v) self.namespaces({k: v}) class Parser(configparser.ConfigParser): """Sub-class of ConfigParser that keeps the case of options and that raises an exception if the file cannot be read. """ def optionxform(self, optionstr): return optionstr def read(self, filenames): if isinstance(filenames, text_or_bytes): filenames = [filenames] for filename in filenames: # try: # fp = open(filename) # except IOError: # continue with open(filename) as fp: self._read(fp, filename) def as_dict(self, raw=False, vars=None): """Convert an INI file to a dictionary""" # Load INI file into a dict result = {} for section in self.sections(): if section not in result: result[section] = {} for option in self.options(section): value = self.get(section, option, raw=raw, vars=vars) try: value = unrepr(value) except Exception: x = sys.exc_info()[1] msg = ('Config error in section: %r, option: %r, ' 'value: %r. Config values must be valid Python.' % (section, option, value)) raise ValueError(msg, x.__class__.__name__, x.args) result[section][option] = value return result def dict_from_file(self, file): if hasattr(file, 'read'): self.read_file(file) else: self.read(file) return self.as_dict() @classmethod def load(self, input): """Resolve 'input' to dict from a dict, file, or filename.""" is_file = ( # Filename isinstance(input, text_or_bytes) # Open file object or hasattr(input, 'read') ) return Parser().dict_from_file(input) if is_file else input.copy() # public domain "unrepr" implementation, found on the web and then improved. class _Builder: def build(self, o): m = getattr(self, 'build_' + o.__class__.__name__, None) if m is None: raise TypeError('unrepr does not recognize %s' % repr(o.__class__.__name__)) return m(o) def astnode(self, s): """Return a Python3 ast Node compiled from a string.""" try: import ast except ImportError: # Fallback to eval when ast package is not available, # e.g. IronPython 1.0. return eval(s) p = ast.parse('__tempvalue__ = ' + s) return p.body[0].value def build_Subscript(self, o): return self.build(o.value)[self.build(o.slice)] def build_Index(self, o): return self.build(o.value) def _build_call35(self, o): """ Workaround for python 3.5 _ast.Call signature, docs found here https://greentreesnakes.readthedocs.org/en/latest/nodes.html """ import ast callee = self.build(o.func) args = [] if o.args is not None: for a in o.args: if isinstance(a, ast.Starred): args.append(self.build(a.value)) else: args.append(self.build(a)) kwargs = {} for kw in o.keywords: if kw.arg is None: # double asterix `**` rst = self.build(kw.value) if not isinstance(rst, dict): raise TypeError('Invalid argument for call.' 'Must be a mapping object.') # give preference to the keys set directly from arg=value for k, v in rst.items(): if k not in kwargs: kwargs[k] = v else: # defined on the call as: arg=value kwargs[kw.arg] = self.build(kw.value) return callee(*args, **kwargs) def build_Call(self, o): if sys.version_info >= (3, 5): return self._build_call35(o) callee = self.build(o.func) if o.args is None: args = () else: args = tuple([self.build(a) for a in o.args]) if o.starargs is None: starargs = () else: starargs = tuple(self.build(o.starargs)) if o.kwargs is None: kwargs = {} else: kwargs = self.build(o.kwargs) if o.keywords is not None: # direct a=b keywords for kw in o.keywords: # preference because is a direct keyword against **kwargs kwargs[kw.arg] = self.build(kw.value) return callee(*(args + starargs), **kwargs) def build_List(self, o): return list(map(self.build, o.elts)) def build_Str(self, o): return o.s def build_Num(self, o): return o.n def build_Dict(self, o): return dict([(self.build(k), self.build(v)) for k, v in zip(o.keys, o.values)]) def build_Tuple(self, o): return tuple(self.build_List(o)) def build_Name(self, o): name = o.id if name == 'None': return None if name == 'True': return True if name == 'False': return False # See if the Name is a package or module. If it is, import it. try: return modules(name) except ImportError: pass # See if the Name is in builtins. try: return getattr(builtins, name) except AttributeError: pass raise TypeError('unrepr could not resolve the name %s' % repr(name)) def build_NameConstant(self, o): return o.value build_Constant = build_NameConstant # Python 3.8 change def build_UnaryOp(self, o): op, operand = map(self.build, [o.op, o.operand]) return op(operand) def build_BinOp(self, o): left, op, right = map(self.build, [o.left, o.op, o.right]) return op(left, right) def build_Add(self, o): return operator.add def build_Mult(self, o): return operator.mul def build_USub(self, o): return operator.neg def build_Attribute(self, o): parent = self.build(o.value) return getattr(parent, o.attr) def build_NoneType(self, o): return None def unrepr(s): """Return a Python object compiled from a string.""" if not s: return s b = _Builder() obj = b.astnode(s) return b.build(obj) def modules(modulePath): """Load a module and retrieve a reference to that module.""" __import__(modulePath) return sys.modules[modulePath] def attributes(full_attribute_name): """Load a module and retrieve an attribute of that module.""" # Parse out the path, module, and attribute last_dot = full_attribute_name.rfind('.') attr_name = full_attribute_name[last_dot + 1:] mod_path = full_attribute_name[:last_dot] mod = modules(mod_path) # Let an AttributeError propagate outward. try: attr = getattr(mod, attr_name) except AttributeError: raise AttributeError("'%s' object has no attribute '%s'" % (mod_path, attr_name)) # Return a reference to the attribute. return attr ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/sessions.py0000644252176402575230000007426014307416152021077 0ustar00jaracoprimarygroup"""Session implementation for CherryPy. You need to edit your config file to use sessions. Here's an example:: [/] tools.sessions.on = True tools.sessions.storage_class = cherrypy.lib.sessions.FileSession tools.sessions.storage_path = "/home/site/sessions" tools.sessions.timeout = 60 This sets the session to be stored in files in the directory /home/site/sessions, and the session timeout to 60 minutes. If you omit ``storage_class``, the sessions will be saved in RAM. ``tools.sessions.on`` is the only required line for working sessions, the rest are optional. By default, the session ID is passed in a cookie, so the client's browser must have cookies enabled for your site. To set data for the current session, use ``cherrypy.session['fieldname'] = 'fieldvalue'``; to get data use ``cherrypy.session.get('fieldname')``. ================ Locking sessions ================ By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means the session is locked early and unlocked late. Be mindful of this default mode for any requests that take a long time to process (streaming responses, expensive calculations, database lookups, API calls, etc), as other concurrent requests that also utilize sessions will hang until the session is unlocked. If you want to control when the session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``. Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. Regardless of which mode you use, the session is guaranteed to be unlocked when the request is complete. ================= Expiring Sessions ================= You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. Simply call that function at the point you want the session to expire, and it will cause the session cookie to expire client-side. =========================== Session Fixation Protection =========================== If CherryPy receives, via a request cookie, a session id that it does not recognize, it will reject that id and create a new one to return in the response cookie. This `helps prevent session fixation attacks `_. However, CherryPy "recognizes" a session id by looking up the saved session data for that id. Therefore, if you never save any session data, **you will get a new session id for every request**. A side effect of CherryPy overwriting unrecognised session ids is that if you have multiple, separate CherryPy applications running on a single domain (e.g. on different ports), each app will overwrite the other's session id because by default they use the same cookie name (``"session_id"``) but do not recognise each others sessions. It is therefore a good idea to use a different name for each, for example:: [/] ... tools.sessions.name = "my_app_session_id" ================ Sharing Sessions ================ If you run multiple instances of CherryPy (for example via mod_python behind Apache prefork), you most likely cannot use the RAM session backend, since each instance of CherryPy will have its own memory space. Use a different backend instead, and verify that all instances are pointing at the same file or db location. Alternately, you might try a load balancer which makes sessions "sticky". Google is your friend, there. ================ Expiration Dates ================ The response cookie will possess an expiration date to inform the client at which point to stop sending the cookie back in requests. If the server time and client time differ, expect sessions to be unreliable. **Make sure the system time of your server is accurate**. CherryPy defaults to a 60-minute session timeout, which also applies to the cookie which is sent to the client. Unfortunately, some versions of Safari ("4 public beta" on Windows XP at least) appear to have a bug in their parsing of the GMT expiration date--they appear to interpret the date as one hour in the past. Sixty minutes minus one hour is pretty close to zero, so you may experience this bug as a new session id for every request, unless the requests are less than one second apart. To fix, try increasing the session.timeout. On the other extreme, some users report Firefox sending cookies after their expiration date, although this was on a system with an inaccurate system time. Maybe FF doesn't trust system time. """ import sys import datetime import os import time import threading import binascii import pickle import zc.lockfile import cherrypy from cherrypy.lib import httputil from cherrypy.lib import locking from cherrypy.lib import is_iterator missing = object() class Session(object): """A CherryPy dict-like Session object (one per request).""" _id = None id_observers = None "A list of callbacks to which to pass new id's." @property def id(self): """Return the current session id.""" return self._id @id.setter def id(self, value): self._id = value for o in self.id_observers: o(value) timeout = 60 'Number of minutes after which to delete session data.' locked = False """ If True, this session instance has exclusive read/write access to session data.""" loaded = False """ If True, data has been retrieved from storage. This should happen automatically on the first attempt to access session data.""" clean_thread = None 'Class-level Monitor which calls self.clean_up.' clean_freq = 5 'The poll rate for expired session cleanup in minutes.' originalid = None 'The session id passed by the client. May be missing or unsafe.' missing = False 'True if the session requested by the client did not exist.' regenerated = False """ True if the application called session.regenerate(). This is not set by internal calls to regenerate the session id.""" debug = False 'If True, log debug information.' # --------------------- Session management methods --------------------- # def __init__(self, id=None, **kwargs): self.id_observers = [] self._data = {} for k, v in kwargs.items(): setattr(self, k, v) self.originalid = id self.missing = False if id is None: if self.debug: cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') self._regenerate() else: self.id = id if self._exists(): if self.debug: cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS') else: if self.debug: cherrypy.log('Expired or malicious session %r; ' 'making a new one' % id, 'TOOLS.SESSIONS') # Expired or malicious session. Make a new one. # See https://github.com/cherrypy/cherrypy/issues/709. self.id = None self.missing = True self._regenerate() def now(self): """Generate the session specific concept of 'now'. Other session providers can override this to use alternative, possibly timezone aware, versions of 'now'. """ return datetime.datetime.now() def regenerate(self): """Replace the current session (with a new id).""" self.regenerated = True self._regenerate() def _regenerate(self): if self.id is not None: if self.debug: cherrypy.log( 'Deleting the existing session %r before ' 'regeneration.' % self.id, 'TOOLS.SESSIONS') self.delete() old_session_was_locked = self.locked if old_session_was_locked: self.release_lock() if self.debug: cherrypy.log('Old lock released.', 'TOOLS.SESSIONS') self.id = None while self.id is None: self.id = self.generate_id() # Assert that the generated id is not already stored. if self._exists(): self.id = None if self.debug: cherrypy.log('Set id to generated %s.' % self.id, 'TOOLS.SESSIONS') if old_session_was_locked: self.acquire_lock() if self.debug: cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS') def clean_up(self): """Clean up expired sessions.""" pass def generate_id(self): """Return a new session id.""" return binascii.hexlify(os.urandom(20)).decode('ascii') def save(self): """Save session data.""" try: # If session data has never been loaded then it's never been # accessed: no need to save it if self.loaded: t = datetime.timedelta(seconds=self.timeout * 60) expiration_time = self.now() + t if self.debug: cherrypy.log('Saving session %r with expiry %s' % (self.id, expiration_time), 'TOOLS.SESSIONS') self._save(expiration_time) else: if self.debug: cherrypy.log( 'Skipping save of session %r (no session loaded).' % self.id, 'TOOLS.SESSIONS') finally: if self.locked: # Always release the lock if the user didn't release it self.release_lock() if self.debug: cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS') def load(self): """Copy stored session data into this session instance.""" data = self._load() # data is either None or a tuple (session_data, expiration_time) if data is None or data[1] < self.now(): if self.debug: cherrypy.log('Expired session %r, flushing data.' % self.id, 'TOOLS.SESSIONS') self._data = {} else: if self.debug: cherrypy.log('Data loaded for session %r.' % self.id, 'TOOLS.SESSIONS') self._data = data[0] self.loaded = True # Stick the clean_thread in the class, not the instance. # The instances are created and destroyed per-request. cls = self.__class__ if self.clean_freq and not cls.clean_thread: # clean_up is an instancemethod and not a classmethod, # so that tool config can be accessed inside the method. t = cherrypy.process.plugins.Monitor( cherrypy.engine, self.clean_up, self.clean_freq * 60, name='Session cleanup') t.subscribe() cls.clean_thread = t t.start() if self.debug: cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS') def delete(self): """Delete stored session data.""" self._delete() if self.debug: cherrypy.log('Deleted session %s.' % self.id, 'TOOLS.SESSIONS') # -------------------- Application accessor methods -------------------- # def __getitem__(self, key): if not self.loaded: self.load() return self._data[key] def __setitem__(self, key, value): if not self.loaded: self.load() self._data[key] = value def __delitem__(self, key): if not self.loaded: self.load() del self._data[key] def pop(self, key, default=missing): """Remove the specified key and return the corresponding value. If key is not found, default is returned if given, otherwise KeyError is raised. """ if not self.loaded: self.load() if default is missing: return self._data.pop(key) else: return self._data.pop(key, default) def __contains__(self, key): if not self.loaded: self.load() return key in self._data def get(self, key, default=None): """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" if not self.loaded: self.load() return self._data.get(key, default) def update(self, d): """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" if not self.loaded: self.load() self._data.update(d) def setdefault(self, key, default=None): """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" if not self.loaded: self.load() return self._data.setdefault(key, default) def clear(self): """D.clear() -> None. Remove all items from D.""" if not self.loaded: self.load() self._data.clear() def keys(self): """D.keys() -> list of D's keys.""" if not self.loaded: self.load() return self._data.keys() def items(self): """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" if not self.loaded: self.load() return self._data.items() def values(self): """D.values() -> list of D's values.""" if not self.loaded: self.load() return self._data.values() class RamSession(Session): # Class-level objects. Don't rebind these! cache = {} locks = {} def clean_up(self): """Clean up expired sessions.""" now = self.now() for _id, (data, expiration_time) in self.cache.copy().items(): if expiration_time <= now: try: del self.cache[_id] except KeyError: pass try: if self.locks[_id].acquire(blocking=False): lock = self.locks.pop(_id) lock.release() except KeyError: pass # added to remove obsolete lock objects for _id in list(self.locks): locked = ( _id not in self.cache and self.locks[_id].acquire(blocking=False) ) if locked: lock = self.locks.pop(_id) lock.release() def _exists(self): return self.id in self.cache def _load(self): return self.cache.get(self.id) def _save(self, expiration_time): self.cache[self.id] = (self._data, expiration_time) def _delete(self): self.cache.pop(self.id, None) def acquire_lock(self): """Acquire an exclusive lock on the currently-loaded session data.""" self.locked = True self.locks.setdefault(self.id, threading.RLock()).acquire() def release_lock(self): """Release the lock on the currently-loaded session data.""" self.locks[self.id].release() self.locked = False def __len__(self): """Return the number of active sessions.""" return len(self.cache) class FileSession(Session): """Implementation of the File backend for sessions storage_path The folder where session data will be saved. Each session will be saved as pickle.dump(data, expiration_time) in its own file; the filename will be self.SESSION_PREFIX + self.id. lock_timeout A timedelta or numeric seconds indicating how long to block acquiring a lock. If None (default), acquiring a lock will block indefinitely. """ SESSION_PREFIX = 'session-' LOCK_SUFFIX = '.lock' pickle_protocol = pickle.HIGHEST_PROTOCOL def __init__(self, id=None, **kwargs): # The 'storage_path' arg is required for file-based sessions. kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) kwargs.setdefault('lock_timeout', None) Session.__init__(self, id=id, **kwargs) # validate self.lock_timeout if isinstance(self.lock_timeout, (int, float)): self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): raise ValueError( 'Lock timeout must be numeric seconds or a timedelta instance.' ) @classmethod def setup(cls, **kwargs): """Set up the storage system for file-based sessions. This should only be called once per process; this will be done automatically when using sessions.init (as the built-in Tool does). """ # The 'storage_path' arg is required for file-based sessions. kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) for k, v in kwargs.items(): setattr(cls, k, v) def _get_file_path(self): f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) if not os.path.abspath(f).startswith(self.storage_path): raise cherrypy.HTTPError(400, 'Invalid session id in cookie.') return f def _exists(self): path = self._get_file_path() return os.path.exists(path) def _load(self, path=None): assert self.locked, ('The session load without being locked. ' "Check your tools' priority levels.") if path is None: path = self._get_file_path() try: with open(path, 'rb') as f: return pickle.load(f) except (IOError, EOFError): e = sys.exc_info()[1] if self.debug: cherrypy.log('Error loading the session pickle: %s' % e, 'TOOLS.SESSIONS') return None def _save(self, expiration_time): assert self.locked, ('The session was saved without being locked. ' "Check your tools' priority levels.") with open(self._get_file_path(), 'wb') as f: pickle.dump((self._data, expiration_time), f, self.pickle_protocol) def _delete(self): assert self.locked, ('The session deletion without being locked. ' "Check your tools' priority levels.") try: os.unlink(self._get_file_path()) except OSError: pass def acquire_lock(self, path=None): """Acquire an exclusive lock on the currently-loaded session data.""" if path is None: path = self._get_file_path() path += self.LOCK_SUFFIX checker = locking.LockChecker(self.id, self.lock_timeout) while not checker.expired(): try: self.lock = zc.lockfile.LockFile(path) except zc.lockfile.LockError: time.sleep(0.1) else: break self.locked = True if self.debug: cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') def release_lock(self, path=None): """Release the lock on the currently-loaded session data.""" self.lock.close() self.locked = False def clean_up(self): """Clean up expired sessions.""" now = self.now() # Iterate over all session files in self.storage_path for fname in os.listdir(self.storage_path): have_session = ( fname.startswith(self.SESSION_PREFIX) and not fname.endswith(self.LOCK_SUFFIX) ) if have_session: # We have a session file: lock and load it and check # if it's expired. If it fails, nevermind. path = os.path.join(self.storage_path, fname) self.acquire_lock(path) if self.debug: # This is a bit of a hack, since we're calling clean_up # on the first instance rather than the entire class, # so depending on whether you have "debug" set on the # path of the first session called, this may not run. cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS') try: contents = self._load(path) # _load returns None on IOError if contents is not None: data, expiration_time = contents if expiration_time < now: # Session expired: deleting it os.unlink(path) finally: self.release_lock(path) def __len__(self): """Return the number of active sessions.""" return len([fname for fname in os.listdir(self.storage_path) if (fname.startswith(self.SESSION_PREFIX) and not fname.endswith(self.LOCK_SUFFIX))]) class MemcachedSession(Session): # The most popular memcached client for Python isn't thread-safe. # Wrap all .get and .set operations in a single lock. mc_lock = threading.RLock() # This is a separate set of locks per session id. locks = {} servers = ['localhost:11211'] @classmethod def setup(cls, **kwargs): """Set up the storage system for memcached-based sessions. This should only be called once per process; this will be done automatically when using sessions.init (as the built-in Tool does). """ for k, v in kwargs.items(): setattr(cls, k, v) import memcache cls.cache = memcache.Client(cls.servers) def _exists(self): self.mc_lock.acquire() try: return bool(self.cache.get(self.id)) finally: self.mc_lock.release() def _load(self): self.mc_lock.acquire() try: return self.cache.get(self.id) finally: self.mc_lock.release() def _save(self, expiration_time): # Send the expiration time as "Unix time" (seconds since 1/1/1970) td = int(time.mktime(expiration_time.timetuple())) self.mc_lock.acquire() try: if not self.cache.set(self.id, (self._data, expiration_time), td): raise AssertionError( 'Session data for id %r not set.' % self.id) finally: self.mc_lock.release() def _delete(self): self.cache.delete(self.id) def acquire_lock(self): """Acquire an exclusive lock on the currently-loaded session data.""" self.locked = True self.locks.setdefault(self.id, threading.RLock()).acquire() if self.debug: cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') def release_lock(self): """Release the lock on the currently-loaded session data.""" self.locks[self.id].release() self.locked = False def __len__(self): """Return the number of active sessions.""" raise NotImplementedError # Hook functions (for CherryPy tools) def save(): """Save any changed session data.""" if not hasattr(cherrypy.serving, 'session'): return request = cherrypy.serving.request response = cherrypy.serving.response # Guard against running twice if hasattr(request, '_sessionsaved'): return request._sessionsaved = True if response.stream: # If the body is being streamed, we have to save the data # *after* the response has been written out request.hooks.attach('on_end_request', cherrypy.session.save) else: # If the body is not being streamed, we save the data now # (so we can release the lock). if is_iterator(response.body): response.collapse_body() cherrypy.session.save() save.failsafe = True def close(): """Close the session object for this request.""" sess = getattr(cherrypy.serving, 'session', None) if getattr(sess, 'locked', False): # If the session is still locked we release the lock sess.release_lock() if sess.debug: cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS') close.failsafe = True close.priority = 90 def init(storage_type=None, path=None, path_header=None, name='session_id', timeout=60, domain=None, secure=False, clean_freq=5, persistent=True, httponly=False, debug=False, # Py27 compat # *, storage_class=RamSession, **kwargs): """Initialize session object (using cookies). storage_class The Session subclass to use. Defaults to RamSession. storage_type (deprecated) One of 'ram', 'file', memcached'. This will be used to look up the corresponding class in cherrypy.lib.sessions globals. For example, 'file' will use the FileSession class. path The 'path' value to stick in the response cookie metadata. path_header If 'path' is None (the default), then the response cookie 'path' will be pulled from request.headers[path_header]. name The name of the cookie. timeout The expiration timeout (in minutes) for the stored session data. If 'persistent' is True (the default), this is also the timeout for the cookie. domain The cookie domain. secure If False (the default) the cookie 'secure' value will not be set. If True, the cookie 'secure' value will be set (to 1). clean_freq (minutes) The poll rate for expired session cleanup. persistent If True (the default), the 'timeout' argument will be used to expire the cookie. If False, the cookie will not have an expiry, and the cookie will be a "session cookie" which expires when the browser is closed. httponly If False (the default) the cookie 'httponly' value will not be set. If True, the cookie 'httponly' value will be set (to 1). Any additional kwargs will be bound to the new Session instance, and may be specific to the storage type. See the subclass of Session you're using for more information. """ # Py27 compat storage_class = kwargs.pop('storage_class', RamSession) request = cherrypy.serving.request # Guard against running twice if hasattr(request, '_session_init_flag'): return request._session_init_flag = True # Check if request came with a session ID id = None if name in request.cookie: id = request.cookie[name].value if debug: cherrypy.log('ID obtained from request.cookie: %r' % id, 'TOOLS.SESSIONS') first_time = not hasattr(cherrypy, 'session') if storage_type: if first_time: msg = 'storage_type is deprecated. Supply storage_class instead' cherrypy.log(msg) storage_class = storage_type.title() + 'Session' storage_class = globals()[storage_class] # call setup first time only if first_time: if hasattr(storage_class, 'setup'): storage_class.setup(**kwargs) # Create and attach a new Session instance to cherrypy.serving. # It will possess a reference to (and lock, and lazily load) # the requested session data. kwargs['timeout'] = timeout kwargs['clean_freq'] = clean_freq cherrypy.serving.session = sess = storage_class(id, **kwargs) sess.debug = debug def update_cookie(id): """Update the cookie every time the session id changes.""" cherrypy.serving.response.cookie[name] = id sess.id_observers.append(update_cookie) # Create cherrypy.session which will proxy to cherrypy.serving.session if not hasattr(cherrypy, 'session'): cherrypy.session = cherrypy._ThreadLocalProxy('session') if persistent: cookie_timeout = timeout else: # See http://support.microsoft.com/kb/223799/EN-US/ # and http://support.mozilla.com/en-US/kb/Cookies cookie_timeout = None set_response_cookie(path=path, path_header=path_header, name=name, timeout=cookie_timeout, domain=domain, secure=secure, httponly=httponly) def set_response_cookie(path=None, path_header=None, name='session_id', timeout=60, domain=None, secure=False, httponly=False): """Set a response cookie for the client. path the 'path' value to stick in the response cookie metadata. path_header if 'path' is None (the default), then the response cookie 'path' will be pulled from request.headers[path_header]. name the name of the cookie. timeout the expiration timeout for the cookie. If 0 or other boolean False, no 'expires' param will be set, and the cookie will be a "session cookie" which expires when the browser is closed. domain the cookie domain. secure if False (the default) the cookie 'secure' value will not be set. If True, the cookie 'secure' value will be set (to 1). httponly If False (the default) the cookie 'httponly' value will not be set. If True, the cookie 'httponly' value will be set (to 1). """ # Set response cookie cookie = cherrypy.serving.response.cookie cookie[name] = cherrypy.serving.session.id cookie[name]['path'] = ( path or cherrypy.serving.request.headers.get(path_header) or '/' ) if timeout: cookie[name]['max-age'] = timeout * 60 _add_MSIE_max_age_workaround(cookie[name], timeout) if domain is not None: cookie[name]['domain'] = domain if secure: cookie[name]['secure'] = 1 if httponly: if not cookie[name].isReservedKey('httponly'): raise ValueError('The httponly cookie token is not supported.') cookie[name]['httponly'] = 1 def _add_MSIE_max_age_workaround(cookie, timeout): """ We'd like to use the "max-age" param as indicated in http://www.faqs.org/rfcs/rfc2109.html but IE doesn't save it to disk and the session is lost if people close the browser. So we have to use the old "expires" ... sigh ... """ expires = time.time() + timeout * 60 cookie['expires'] = httputil.HTTPDate(expires) def expire(): """Expire the current session cookie.""" name = cherrypy.serving.request.config.get( 'tools.sessions.name', 'session_id') one_year = 60 * 60 * 24 * 365 e = time.time() - one_year cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) cherrypy.serving.response.cookie[name].pop('max-age', None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/cherrypy/lib/static.py0000644252176402575230000004034414424762003020513 0ustar00jaracoprimarygroup"""Module with helpers for serving static files.""" import mimetypes import os import platform import re import stat import unicodedata import urllib.parse from email.generator import _make_boundary as make_boundary from io import UnsupportedOperation import cherrypy from cherrypy._cpcompat import ntob from cherrypy.lib import cptools, file_generator_limited, httputil def _setup_mimetypes(): """Pre-initialize global mimetype map.""" if not mimetypes.inited: mimetypes.init() mimetypes.types_map['.dwg'] = 'image/x-dwg' mimetypes.types_map['.ico'] = 'image/x-icon' mimetypes.types_map['.bz2'] = 'application/x-bzip2' mimetypes.types_map['.gz'] = 'application/x-gzip' _setup_mimetypes() def _make_content_disposition(disposition, file_name): """Create HTTP header for downloading a file with a UTF-8 filename. This function implements the recommendations of :rfc:`6266#appendix-D`. See this and related answers: https://stackoverflow.com/a/8996249/2173868. """ # As normalization algorithm for `unicodedata` is used composed form (NFC # and NFKC) with compatibility equivalence criteria (NFK), so "NFKC" is the # one. It first applies the compatibility decomposition, followed by the # canonical composition. Should be displayed in the same manner, should be # treated in the same way by applications such as alphabetizing names or # searching, and may be substituted for each other. # See: https://en.wikipedia.org/wiki/Unicode_equivalence. ascii_name = ( unicodedata.normalize('NFKC', file_name). encode('ascii', errors='ignore').decode() ) header = '{}; filename="{}"'.format(disposition, ascii_name) if ascii_name != file_name: quoted_name = urllib.parse.quote(file_name) header += '; filename*=UTF-8\'\'{}'.format(quoted_name) return header def serve_file(path, content_type=None, disposition=None, name=None, debug=False): """Set status, headers, and body in order to serve the given path. The Content-Type header will be set to the content_type arg, if provided. If not provided, the Content-Type will be guessed by the file extension of the 'path' argument. If disposition is not None, the Content-Disposition header will be set to "; filename=; filename*=utf-8''" as described in :rfc:`6266#appendix-D`. If name is None, it will be set to the basename of path. If disposition is None, no Content-Disposition header will be written. """ response = cherrypy.serving.response # If path is relative, users should fix it by making path absolute. # That is, CherryPy should not guess where the application root is. # It certainly should *not* use cwd (since CP may be invoked from a # variety of paths). If using tools.staticdir, you can make your relative # paths become absolute by supplying a value for "tools.staticdir.root". if not os.path.isabs(path): msg = "'%s' is not an absolute path." % path if debug: cherrypy.log(msg, 'TOOLS.STATICFILE') raise ValueError(msg) try: st = os.stat(path) except (OSError, TypeError, ValueError): # OSError when file fails to stat # TypeError on Python 2 when there's a null byte # ValueError on Python 3 when there's a null byte if debug: cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') raise cherrypy.NotFound() # Check if path is a directory. if stat.S_ISDIR(st.st_mode): # Let the caller deal with it as they like. if debug: cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') raise cherrypy.NotFound() # Set the Last-Modified response header, so that # modified-since validation code can work. response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) cptools.validate_since() if content_type is None: # Set content-type based on filename extension ext = '' i = path.rfind('.') if i != -1: ext = path[i:].lower() content_type = mimetypes.types_map.get(ext, None) if content_type is not None: response.headers['Content-Type'] = content_type if debug: cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') cd = None if disposition is not None: if name is None: name = os.path.basename(path) cd = _make_content_disposition(disposition, name) response.headers['Content-Disposition'] = cd if debug: cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') # Set Content-Length and use an iterable (file object) # this way CP won't load the whole file in memory content_length = st.st_size fileobj = open(path, 'rb') return _serve_fileobj(fileobj, content_type, content_length, debug=debug) def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, debug=False): """Set status, headers, and body in order to serve the given file object. The Content-Type header will be set to the content_type arg, if provided. If disposition is not None, the Content-Disposition header will be set to "; filename=; filename*=utf-8''" as described in :rfc:`6266#appendix-D`. If name is None, 'filename' will not be set. If disposition is None, no Content-Disposition header will be written. CAUTION: If the request contains a 'Range' header, one or more seek()s will be performed on the file object. This may cause undesired behavior if the file object is not seekable. It could also produce undesired results if the caller set the read position of the file object prior to calling serve_fileobj(), expecting that the data would be served starting from that position. """ response = cherrypy.serving.response try: st = os.fstat(fileobj.fileno()) except AttributeError: if debug: cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') content_length = None except UnsupportedOperation: content_length = None else: # Set the Last-Modified response header, so that # modified-since validation code can work. response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) cptools.validate_since() content_length = st.st_size if content_type is not None: response.headers['Content-Type'] = content_type if debug: cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') cd = None if disposition is not None: if name is None: cd = disposition else: cd = _make_content_disposition(disposition, name) response.headers['Content-Disposition'] = cd if debug: cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') return _serve_fileobj(fileobj, content_type, content_length, debug=debug) def _serve_fileobj(fileobj, content_type, content_length, debug=False): """Set ``response.body`` to the given file object, perhaps ranged. Internal helper. """ response = cherrypy.serving.response # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code request = cherrypy.serving.request if request.protocol >= (1, 1): response.headers['Accept-Ranges'] = 'bytes' r = httputil.get_ranges(request.headers.get('Range'), content_length) if r == []: response.headers['Content-Range'] = 'bytes */%s' % content_length message = ('Invalid Range (first-byte-pos greater than ' 'Content-Length)') if debug: cherrypy.log(message, 'TOOLS.STATIC') raise cherrypy.HTTPError(416, message) if r: if len(r) == 1: # Return a single-part response. start, stop = r[0] if stop > content_length: stop = content_length r_len = stop - start if debug: cherrypy.log( 'Single part; start: %r, stop: %r' % (start, stop), 'TOOLS.STATIC') response.status = '206 Partial Content' response.headers['Content-Range'] = ( 'bytes %s-%s/%s' % (start, stop - 1, content_length)) response.headers['Content-Length'] = r_len fileobj.seek(start) response.body = file_generator_limited(fileobj, r_len) else: # Return a multipart/byteranges response. response.status = '206 Partial Content' boundary = make_boundary() ct = 'multipart/byteranges; boundary=%s' % boundary response.headers['Content-Type'] = ct if 'Content-Length' in response.headers: # Delete Content-Length header so finalize() recalcs it. del response.headers['Content-Length'] def file_ranges(): # Apache compatibility: yield b'\r\n' for start, stop in r: if debug: cherrypy.log( 'Multipart; start: %r, stop: %r' % ( start, stop), 'TOOLS.STATIC') yield ntob('--' + boundary, 'ascii') yield ntob('\r\nContent-type: %s' % content_type, 'ascii') yield ntob( '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % ( start, stop - 1, content_length), 'ascii') fileobj.seek(start) gen = file_generator_limited(fileobj, stop - start) for chunk in gen: yield chunk yield b'\r\n' # Final boundary yield ntob('--' + boundary + '--', 'ascii') # Apache compatibility: yield b'\r\n' response.body = file_ranges() return response.body else: if debug: cherrypy.log('No byteranges requested', 'TOOLS.STATIC') # Set Content-Length and use an iterable (file object) # this way CP won't load the whole file in memory response.headers['Content-Length'] = content_length response.body = fileobj return response.body def serve_download(path, name=None): """Serve 'path' as an application/x-download attachment.""" # This is such a common idiom I felt it deserved its own wrapper. return serve_file(path, 'application/x-download', 'attachment', name) def _attempt(filename, content_types, debug=False): if debug: cherrypy.log('Attempting %r (content_types %r)' % (filename, content_types), 'TOOLS.STATICDIR') try: # you can set the content types for a # complete directory per extension content_type = None if content_types: r, ext = os.path.splitext(filename) content_type = content_types.get(ext[1:], None) serve_file(filename, content_type=content_type, debug=debug) return True except cherrypy.NotFound: # If we didn't find the static file, continue handling the # request. We might find a dynamic handler instead. if debug: cherrypy.log('NotFound', 'TOOLS.STATICFILE') return False def staticdir(section, dir, root='', match='', content_types=None, index='', debug=False): """Serve a static resource from the given (root +) dir. match If given, request.path_info will be searched for the given regular expression before attempting to serve static content. content_types If given, it should be a Python dictionary of {file-extension: content-type} pairs, where 'file-extension' is a string (e.g. "gif") and 'content-type' is the value to write out in the Content-Type response header (e.g. "image/gif"). index If provided, it should be the (relative) name of a file to serve for directory requests. For example, if the dir argument is '/home/me', the Request-URI is 'myapp', and the index arg is 'index.html', the file '/home/me/myapp/index.html' will be sought. """ request = cherrypy.serving.request if request.method not in ('GET', 'HEAD'): if debug: cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') return False if match and not re.search(match, request.path_info): if debug: cherrypy.log('request.path_info %r does not match pattern %r' % (request.path_info, match), 'TOOLS.STATICDIR') return False # Allow the use of '~' to refer to a user's home directory. dir = os.path.expanduser(dir) # If dir is relative, make absolute using "root". if not os.path.isabs(dir): if not root: msg = 'Static dir requires an absolute dir (or root).' if debug: cherrypy.log(msg, 'TOOLS.STATICDIR') raise ValueError(msg) dir = os.path.join(root, dir) # Determine where we are in the object tree relative to 'section' # (where the static tool was defined). if section == 'global': section = '/' section = section.rstrip(r'\/') branch = request.path_info[len(section) + 1:] branch = urllib.parse.unquote(branch.lstrip(r'\/')) # Requesting a file in sub-dir of the staticdir results # in mixing of delimiter styles, e.g. C:\static\js/script.js. # Windows accepts this form except not when the path is # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js. # http://bit.ly/1vdioCX if platform.system() == 'Windows': branch = branch.replace('/', '\\') # If branch is "", filename will end in a slash filename = os.path.join(dir, branch) if debug: cherrypy.log('Checking file %r to fulfill %r' % (filename, request.path_info), 'TOOLS.STATICDIR') # There's a chance that the branch pulled from the URL might # have ".." or similar uplevel attacks in it. Check that the final # filename is a child of dir. if not os.path.normpath(filename).startswith(os.path.normpath(dir)): raise cherrypy.HTTPError(403) # Forbidden handled = _attempt(filename, content_types) if not handled: # Check for an index file if a folder was requested. if index: handled = _attempt(os.path.join(filename, index), content_types) if handled: request.is_index = filename[-1] in (r'\/') return handled def staticfile(filename, root=None, match='', content_types=None, debug=False): """Serve a static resource from the given (root +) filename. match If given, request.path_info will be searched for the given regular expression before attempting to serve static content. content_types If given, it should be a Python dictionary of {file-extension: content-type} pairs, where 'file-extension' is a string (e.g. "gif") and 'content-type' is the value to write out in the Content-Type response header (e.g. "image/gif"). """ request = cherrypy.serving.request if request.method not in ('GET', 'HEAD'): if debug: cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') return False if match and not re.search(match, request.path_info): if debug: cherrypy.log('request.path_info %r does not match pattern %r' % (request.path_info, match), 'TOOLS.STATICFILE') return False # If filename is relative, make absolute using "root". if not os.path.isabs(filename): if not root: msg = "Static tool requires an absolute filename (got '%s')." % ( filename,) if debug: cherrypy.log(msg, 'TOOLS.STATICFILE') raise ValueError(msg) filename = os.path.join(root, filename) return _attempt(filename, content_types, debug=debug) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/lib/xmlrpcutil.py0000644252176402575230000000322414307416152021424 0ustar00jaracoprimarygroup"""XML-RPC tool helpers.""" import sys from xmlrpc.client import ( loads as xmlrpc_loads, dumps as xmlrpc_dumps, Fault as XMLRPCFault ) import cherrypy from cherrypy._cpcompat import ntob def process_body(): """Return (params, method) from request body.""" try: return xmlrpc_loads(cherrypy.request.body.read()) except Exception: return ('ERROR PARAMS', ), 'ERRORMETHOD' def patched_path(path): """Return 'path', doctored for RPC.""" if not path.endswith('/'): path += '/' if path.startswith('/RPC2/'): # strip the first /rpc2 path = path[5:] return path def _set_response(body): """Set up HTTP status, headers and body within CherryPy.""" # The XML-RPC spec (http://www.xmlrpc.com/spec) says: # "Unless there's a lower-level error, always return 200 OK." # Since Python's xmlrpc_client interprets a non-200 response # as a "Protocol Error", we'll just return 200 every time. response = cherrypy.response response.status = '200 OK' response.body = ntob(body, 'utf-8') response.headers['Content-Type'] = 'text/xml' response.headers['Content-Length'] = len(body) def respond(body, encoding='utf-8', allow_none=0): """Construct HTTP response body.""" if not isinstance(body, XMLRPCFault): body = (body,) _set_response( xmlrpc_dumps( body, methodresponse=1, encoding=encoding, allow_none=allow_none ) ) def on_error(*args, **kwargs): """Construct HTTP response body for an error response.""" body = str(sys.exc_info()[1]) _set_response(xmlrpc_dumps(XMLRPCFault(1, body))) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5282829 CherryPy-18.9.0/cherrypy/process/0000755252176402575230000000000014536340235017560 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/process/__init__.py0000644252176402575230000000104314307416152021665 0ustar00jaracoprimarygroup"""Site container for an HTTP server. A Web Site Process Bus object is used to connect applications, servers, and frameworks with site-wide services such as daemonization, process reload, signal handling, drop privileges, PID file management, logging for all of these, and many more. The 'plugins' module defines a few abstract and concrete services for use with the bus. Some use tool-specific channels; see the documentation for each class. """ from .wspbus import bus from . import plugins, servers __all__ = ('bus', 'plugins', 'servers') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/process/plugins.py0000644252176402575230000006433614307416152021625 0ustar00jaracoprimarygroup"""Site services for use with a Web Site Process Bus.""" import os import re import signal as _signal import sys import time import threading import _thread from cherrypy._cpcompat import text_or_bytes from cherrypy._cpcompat import ntob # _module__file__base is used by Autoreload to make # absolute any filenames retrieved from sys.modules which are not # already absolute paths. This is to work around Python's quirk # of importing the startup script and using a relative filename # for it in sys.modules. # # Autoreload examines sys.modules afresh every time it runs. If an application # changes the current directory by executing os.chdir(), then the next time # Autoreload runs, it will not be able to find any filenames which are # not absolute paths, because the current directory is not the same as when the # module was first imported. Autoreload will then wrongly conclude the file # has "changed", and initiate the shutdown/re-exec sequence. # See ticket #917. # For this workaround to have a decent probability of success, this module # needs to be imported as early as possible, before the app has much chance # to change the working directory. _module__file__base = os.getcwd() class SimplePlugin(object): """Plugin base class which auto-subscribes methods for known channels.""" bus = None """A :class:`Bus `, usually cherrypy.engine. """ def __init__(self, bus): self.bus = bus def subscribe(self): """Register this object as a (multi-channel) listener on the bus.""" for channel in self.bus.listeners: # Subscribe self.start, self.exit, etc. if present. method = getattr(self, channel, None) if method is not None: self.bus.subscribe(channel, method) def unsubscribe(self): """Unregister this object as a listener on the bus.""" for channel in self.bus.listeners: # Unsubscribe self.start, self.exit, etc. if present. method = getattr(self, channel, None) if method is not None: self.bus.unsubscribe(channel, method) class SignalHandler(object): """Register bus channels (and listeners) for system signals. You can modify what signals your application listens for, and what it does when it receives signals, by modifying :attr:`SignalHandler.handlers`, a dict of {signal name: callback} pairs. The default set is:: handlers = {'SIGTERM': self.bus.exit, 'SIGHUP': self.handle_SIGHUP, 'SIGUSR1': self.bus.graceful, } The :func:`SignalHandler.handle_SIGHUP`` method calls :func:`bus.restart()` if the process is daemonized, but :func:`bus.exit()` if the process is attached to a TTY. This is because Unix window managers tend to send SIGHUP to terminal windows when the user closes them. Feel free to add signals which are not available on every platform. The :class:`SignalHandler` will ignore errors raised from attempting to register handlers for unknown signals. """ handlers = {} """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" signals = {} """A map from signal numbers to names.""" for k, v in vars(_signal).items(): if k.startswith('SIG') and not k.startswith('SIG_'): signals[v] = k del k, v def __init__(self, bus): self.bus = bus # Set default handlers self.handlers = {'SIGTERM': self.bus.exit, 'SIGHUP': self.handle_SIGHUP, 'SIGUSR1': self.bus.graceful, } if sys.platform[:4] == 'java': del self.handlers['SIGUSR1'] self.handlers['SIGUSR2'] = self.bus.graceful self.bus.log('SIGUSR1 cannot be set on the JVM platform. ' 'Using SIGUSR2 instead.') self.handlers['SIGINT'] = self._jython_SIGINT_handler self._previous_handlers = {} # used to determine is the process is a daemon in `self._is_daemonized` self._original_pid = os.getpid() def _jython_SIGINT_handler(self, signum=None, frame=None): # See http://bugs.jython.org/issue1313 self.bus.log('Keyboard Interrupt: shutting down bus') self.bus.exit() def _is_daemonized(self): """Return boolean indicating if the current process is running as a daemon. The criteria to determine the `daemon` condition is to verify if the current pid is not the same as the one that got used on the initial construction of the plugin *and* the stdin is not connected to a terminal. The sole validation of the tty is not enough when the plugin is executing inside other process like in a CI tool (Buildbot, Jenkins). """ return ( self._original_pid != os.getpid() and not os.isatty(sys.stdin.fileno()) ) def subscribe(self): """Subscribe self.handlers to signals.""" for sig, func in self.handlers.items(): try: self.set_handler(sig, func) except ValueError: pass def unsubscribe(self): """Unsubscribe self.handlers from signals.""" for signum, handler in self._previous_handlers.items(): signame = self.signals[signum] if handler is None: self.bus.log('Restoring %s handler to SIG_DFL.' % signame) handler = _signal.SIG_DFL else: self.bus.log('Restoring %s handler %r.' % (signame, handler)) try: our_handler = _signal.signal(signum, handler) if our_handler is None: self.bus.log('Restored old %s handler %r, but our ' 'handler was not registered.' % (signame, handler), level=30) except ValueError: self.bus.log('Unable to restore %s handler %r.' % (signame, handler), level=40, traceback=True) def set_handler(self, signal, listener=None): """Subscribe a handler for the given signal (number or name). If the optional 'listener' argument is provided, it will be subscribed as a listener for the given signal's channel. If the given signal name or number is not available on the current platform, ValueError is raised. """ if isinstance(signal, text_or_bytes): signum = getattr(_signal, signal, None) if signum is None: raise ValueError('No such signal: %r' % signal) signame = signal else: try: signame = self.signals[signal] except KeyError: raise ValueError('No such signal: %r' % signal) signum = signal prev = _signal.signal(signum, self._handle_signal) self._previous_handlers[signum] = prev if listener is not None: self.bus.log('Listening for %s.' % signame) self.bus.subscribe(signame, listener) def _handle_signal(self, signum=None, frame=None): """Python signal handler (self.set_handler subscribes it for you).""" signame = self.signals[signum] self.bus.log('Caught signal %s.' % signame) self.bus.publish(signame) def handle_SIGHUP(self): """Restart if daemonized, else exit.""" if self._is_daemonized(): self.bus.log('SIGHUP caught while daemonized. Restarting.') self.bus.restart() else: # not daemonized (may be foreground or background) self.bus.log('SIGHUP caught but not daemonized. Exiting.') self.bus.exit() try: import pwd import grp except ImportError: pwd, grp = None, None class DropPrivileges(SimplePlugin): """Drop privileges. uid/gid arguments not available on Windows. Special thanks to `Gavin Baker `_ """ def __init__(self, bus, umask=None, uid=None, gid=None): SimplePlugin.__init__(self, bus) self.finalized = False self.uid = uid self.gid = gid self.umask = umask @property def uid(self): """The uid under which to run. Availability: Unix.""" return self._uid @uid.setter def uid(self, val): if val is not None: if pwd is None: self.bus.log('pwd module not available; ignoring uid.', level=30) val = None elif isinstance(val, text_or_bytes): val = pwd.getpwnam(val)[2] self._uid = val @property def gid(self): """The gid under which to run. Availability: Unix.""" return self._gid @gid.setter def gid(self, val): if val is not None: if grp is None: self.bus.log('grp module not available; ignoring gid.', level=30) val = None elif isinstance(val, text_or_bytes): val = grp.getgrnam(val)[2] self._gid = val @property def umask(self): """The default permission mode for newly created files and directories. Usually expressed in octal format, for example, ``0644``. Availability: Unix, Windows. """ return self._umask @umask.setter def umask(self, val): if val is not None: try: os.umask except AttributeError: self.bus.log('umask function not available; ignoring umask.', level=30) val = None self._umask = val def start(self): # uid/gid def current_ids(): """Return the current (uid, gid) if available.""" name, group = None, None if pwd: name = pwd.getpwuid(os.getuid())[0] if grp: group = grp.getgrgid(os.getgid())[0] return name, group if self.finalized: if not (self.uid is None and self.gid is None): self.bus.log('Already running as uid: %r gid: %r' % current_ids()) else: if self.uid is None and self.gid is None: if pwd or grp: self.bus.log('uid/gid not set', level=30) else: self.bus.log('Started as uid: %r gid: %r' % current_ids()) if self.gid is not None: os.setgid(self.gid) os.setgroups([]) if self.uid is not None: os.setuid(self.uid) self.bus.log('Running as uid: %r gid: %r' % current_ids()) # umask if self.finalized: if self.umask is not None: self.bus.log('umask already set to: %03o' % self.umask) else: if self.umask is None: self.bus.log('umask not set', level=30) else: old_umask = os.umask(self.umask) self.bus.log('umask old: %03o, new: %03o' % (old_umask, self.umask)) self.finalized = True # This is slightly higher than the priority for server.start # in order to facilitate the most common use: starting on a low # port (which requires root) and then dropping to another user. start.priority = 77 class Daemonizer(SimplePlugin): """Daemonize the running script. Use this with a Web Site Process Bus via:: Daemonizer(bus).subscribe() When this component finishes, the process is completely decoupled from the parent environment. Please note that when this component is used, the return code from the parent process will still be 0 if a startup error occurs in the forked children. Errors in the initial daemonizing process still return proper exit codes. Therefore, if you use this plugin to daemonize, don't use the return code as an accurate indicator of whether the process fully started. In fact, that return code only indicates if the process successfully finished the first fork. """ def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): SimplePlugin.__init__(self, bus) self.stdin = stdin self.stdout = stdout self.stderr = stderr self.finalized = False def start(self): if self.finalized: self.bus.log('Already deamonized.') # forking has issues with threads: # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html # "The general problem with making fork() work in a multi-threaded # world is what to do with all of the threads..." # So we check for active threads: if threading.active_count() != 1: self.bus.log('There are %r active threads. ' 'Daemonizing now may cause strange failures.' % threading.enumerate(), level=30) self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log) self.finalized = True start.priority = 65 @staticmethod def daemonize( stdin='/dev/null', stdout='/dev/null', stderr='/dev/null', logger=lambda msg: None): # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 # Finish up with the current stdout/stderr sys.stdout.flush() sys.stderr.flush() error_tmpl = ( '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n' ) for fork in range(2): msg = ['Forking once.', 'Forking twice.'][fork] try: pid = os.fork() if pid > 0: # This is the parent; exit. logger(msg) os._exit(0) except OSError as exc: # Python raises OSError rather than returning negative numbers. sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1)) if fork == 0: os.setsid() os.umask(0) si = open(stdin, 'r') so = open(stdout, 'a+') se = open(stderr, 'a+') # os.dup2(fd, fd2) will close fd2 if necessary, # so we don't explicitly close stdin/out/err. # See http://docs.python.org/lib/os-fd-ops.html os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) logger('Daemonized to PID: %s' % os.getpid()) class PIDFile(SimplePlugin): """Maintain a PID file via a WSPBus.""" def __init__(self, bus, pidfile): SimplePlugin.__init__(self, bus) self.pidfile = pidfile self.finalized = False def start(self): pid = os.getpid() if self.finalized: self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) else: with open(self.pidfile, 'wb') as f: f.write(ntob('%s\n' % pid, 'utf8')) self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) self.finalized = True start.priority = 70 def exit(self): try: os.remove(self.pidfile) self.bus.log('PID file removed: %r.' % self.pidfile) except (KeyboardInterrupt, SystemExit): raise except Exception: pass class PerpetualTimer(threading.Timer): """A responsive subclass of threading.Timer whose run() method repeats. Use this timer only when you really need a very interruptible timer; this checks its 'finished' condition up to 20 times a second, which can results in pretty high CPU usage """ def __init__(self, *args, **kwargs): "Override parent constructor to allow 'bus' to be provided." self.bus = kwargs.pop('bus', None) super(PerpetualTimer, self).__init__(*args, **kwargs) def run(self): while True: self.finished.wait(self.interval) if self.finished.isSet(): return try: self.function(*self.args, **self.kwargs) except Exception: if self.bus: self.bus.log( 'Error in perpetual timer thread function %r.' % self.function, level=40, traceback=True) # Quit on first error to avoid massive logs. raise class BackgroundTask(threading.Thread): """A subclass of threading.Thread whose run() method repeats. Use this class for most repeating tasks. It uses time.sleep() to wait for each interval, which isn't very responsive; that is, even if you call self.cancel(), you'll have to wait until the sleep() call finishes before the thread stops. To compensate, it defaults to being daemonic, which means it won't delay stopping the whole process. """ def __init__(self, interval, function, args=[], kwargs={}, bus=None): super(BackgroundTask, self).__init__() self.interval = interval self.function = function self.args = args self.kwargs = kwargs self.running = False self.bus = bus # default to daemonic self.daemon = True def cancel(self): self.running = False def run(self): self.running = True while self.running: time.sleep(self.interval) if not self.running: return try: self.function(*self.args, **self.kwargs) except Exception: if self.bus: self.bus.log('Error in background task thread function %r.' % self.function, level=40, traceback=True) # Quit on first error to avoid massive logs. raise class Monitor(SimplePlugin): """WSPBus listener to periodically run a callback in its own thread.""" callback = None """The function to call at intervals.""" frequency = 60 """The time in seconds between callback runs.""" thread = None """A :class:`BackgroundTask` thread. """ def __init__(self, bus, callback, frequency=60, name=None): SimplePlugin.__init__(self, bus) self.callback = callback self.frequency = frequency self.thread = None self.name = name def start(self): """Start our callback in its own background thread.""" if self.frequency > 0: threadname = self.name or self.__class__.__name__ if self.thread is None: self.thread = BackgroundTask(self.frequency, self.callback, bus=self.bus) self.thread.name = threadname self.thread.start() self.bus.log('Started monitor thread %r.' % threadname) else: self.bus.log('Monitor thread %r already started.' % threadname) start.priority = 70 def stop(self): """Stop our callback's background task thread.""" if self.thread is None: self.bus.log('No thread running for %s.' % self.name or self.__class__.__name__) else: if self.thread is not threading.current_thread(): name = self.thread.name self.thread.cancel() if not self.thread.daemon: self.bus.log('Joining %r' % name) self.thread.join() self.bus.log('Stopped thread %r.' % name) self.thread = None def graceful(self): """Stop the callback's background task thread and restart it.""" self.stop() self.start() class Autoreloader(Monitor): """Monitor which re-executes the process when files change. This :ref:`plugin` restarts the process (via :func:`os.execv`) if any of the files it monitors change (or is deleted). By default, the autoreloader monitors all imported modules; you can add to the set by adding to ``autoreload.files``:: cherrypy.engine.autoreload.files.add(myFile) If there are imported files you do *not* wish to monitor, you can adjust the ``match`` attribute, a regular expression. For example, to stop monitoring cherrypy itself:: cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' Like all :class:`Monitor` plugins, the autoreload plugin takes a ``frequency`` argument. The default is 1 second; that is, the autoreloader will examine files once each second. """ files = None """The set of files to poll for modifications.""" frequency = 1 """The interval in seconds at which to poll for modified files.""" match = '.*' """A regular expression by which to match filenames.""" def __init__(self, bus, frequency=1, match='.*'): self.mtimes = {} self.files = set() self.match = match Monitor.__init__(self, bus, self.run, frequency) def start(self): """Start our own background task thread for self.run.""" if self.thread is None: self.mtimes = {} Monitor.start(self) start.priority = 70 def sysfiles(self): """Return a Set of sys.modules filenames to monitor.""" search_mod_names = filter( re.compile(self.match).match, list(sys.modules.keys()), ) mods = map(sys.modules.get, search_mod_names) return set(filter(None, map(self._file_for_module, mods))) @classmethod def _file_for_module(cls, module): """Return the relevant file for the module.""" return ( cls._archive_for_zip_module(module) or cls._file_for_file_module(module) ) @staticmethod def _archive_for_zip_module(module): """Return the archive filename for the module if relevant.""" try: return module.__loader__.archive except AttributeError: pass @classmethod def _file_for_file_module(cls, module): """Return the file for the module.""" try: return module.__file__ and cls._make_absolute(module.__file__) except AttributeError: pass @staticmethod def _make_absolute(filename): """Ensure filename is absolute to avoid effect of os.chdir.""" return filename if os.path.isabs(filename) else ( os.path.normpath(os.path.join(_module__file__base, filename)) ) def run(self): """Reload the process if registered files have been modified.""" for filename in self.sysfiles() | self.files: if filename: if filename.endswith('.pyc'): filename = filename[:-1] oldtime = self.mtimes.get(filename, 0) if oldtime is None: # Module with no .py file. Skip it. continue try: mtime = os.stat(filename).st_mtime except OSError: # Either a module with no .py file, or it's been deleted. mtime = None if filename not in self.mtimes: # If a module has no .py file, this will be None. self.mtimes[filename] = mtime else: if mtime is None or mtime > oldtime: # The file has been deleted or modified. self.bus.log('Restarting because %s changed.' % filename) self.thread.cancel() self.bus.log('Stopped thread %r.' % self.thread.name) self.bus.restart() return class ThreadManager(SimplePlugin): """Manager for HTTP request threads. If you have control over thread creation and destruction, publish to the 'acquire_thread' and 'release_thread' channels (for each thread). This will register/unregister the current thread and publish to 'start_thread' and 'stop_thread' listeners in the bus as needed. If threads are created and destroyed by code you do not control (e.g., Apache), then, at the beginning of every HTTP request, publish to 'acquire_thread' only. You should not publish to 'release_thread' in this case, since you do not know whether the thread will be re-used or not. The bus will call 'stop_thread' listeners for you when it stops. """ threads = None """A map of {thread ident: index number} pairs.""" def __init__(self, bus): self.threads = {} SimplePlugin.__init__(self, bus) self.bus.listeners.setdefault('acquire_thread', set()) self.bus.listeners.setdefault('start_thread', set()) self.bus.listeners.setdefault('release_thread', set()) self.bus.listeners.setdefault('stop_thread', set()) def acquire_thread(self): """Run 'start_thread' listeners for the current thread. If the current thread has already been seen, any 'start_thread' listeners will not be run again. """ thread_ident = _thread.get_ident() if thread_ident not in self.threads: # We can't just use get_ident as the thread ID # because some platforms reuse thread ID's. i = len(self.threads) + 1 self.threads[thread_ident] = i self.bus.publish('start_thread', i) def release_thread(self): """Release the current thread and run 'stop_thread' listeners.""" thread_ident = _thread.get_ident() i = self.threads.pop(thread_ident, None) if i is not None: self.bus.publish('stop_thread', i) def stop(self): """Release all threads and run all 'stop_thread' listeners.""" for thread_ident, i in self.threads.items(): self.bus.publish('stop_thread', i) self.threads.clear() graceful = stop ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/process/servers.py0000644252176402575230000003220214307416152021620 0ustar00jaracoprimarygroupr""" Starting in CherryPy 3.1, cherrypy.server is implemented as an :ref:`Engine Plugin`. It's an instance of :class:`cherrypy._cpserver.Server`, which is a subclass of :class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class is designed to control other servers, as well. Multiple servers/ports ====================== If you need to start more than one HTTP server (to serve on multiple ports, or protocols, etc.), you can manually register each one and then start them all with engine.start:: s1 = ServerAdapter( cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80) ) s2 = ServerAdapter( cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True) ) s1.subscribe() s2.subscribe() cherrypy.engine.start() .. index:: SCGI FastCGI/SCGI ============ There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in :mod:`cherrypy.process.servers`. To start an fcgi server, for example, wrap an instance of it in a ServerAdapter:: addr = ('0.0.0.0', 4000) f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) s.subscribe() The :doc:`cherryd` startup script will do the above for you via its `-f` flag. Note that you need to download and install `flup `_ yourself, whether you use ``cherryd`` or not. .. _fastcgi: .. index:: FastCGI FastCGI ------- A very simple setup lets your cherry run with FastCGI. You just need the flup library, plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. CherryPy code ^^^^^^^^^^^^^ hello.py:: #!/usr/bin/python import cherrypy class HelloWorld: '''Sample request handler class.''' @cherrypy.expose def index(self): return "Hello world!" cherrypy.tree.mount(HelloWorld()) # CherryPy autoreload must be disabled for the flup server to work cherrypy.config.update({'engine.autoreload.on':False}) Then run :doc:`/deployguide/cherryd` with the '-f' arg:: cherryd -c -d -f -i hello.py Apache ^^^^^^ At the top level in httpd.conf:: FastCgiIpcDir /tmp FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 And inside the relevant VirtualHost section:: # FastCGI config AddHandler fastcgi-script .fcgi ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 Lighttpd ^^^^^^^^ For `Lighttpd `_ you can follow these instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is active within ``server.modules``. Then, within your ``$HTTP["host"]`` directive, configure your fastcgi script like the following:: $HTTP["url"] =~ "" { fastcgi.server = ( "/" => ( "script.fcgi" => ( "bin-path" => "/path/to/your/script.fcgi", "socket" => "/tmp/script.sock", "check-local" => "disable", "disable-time" => 1, "min-procs" => 1, "max-procs" => 1, # adjust as needed ), ), ) } # end of $HTTP["url"] =~ "^/" Please see `Lighttpd FastCGI Docs `_ for an explanation of the possible configuration options. """ import os import sys import time import warnings import contextlib import portend class Timeouts: occupied = 5 free = 1 class ServerAdapter(object): """Adapter for an HTTP server. If you need to start more than one HTTP server (to serve on multiple ports, or protocols, etc.), you can manually register each one and then start them all with bus.start:: s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) s1.subscribe() s2.subscribe() bus.start() """ def __init__(self, bus, httpserver=None, bind_addr=None): self.bus = bus self.httpserver = httpserver self.bind_addr = bind_addr self.interrupt = None self.running = False def subscribe(self): self.bus.subscribe('start', self.start) self.bus.subscribe('stop', self.stop) def unsubscribe(self): self.bus.unsubscribe('start', self.start) self.bus.unsubscribe('stop', self.stop) def start(self): """Start the HTTP server.""" if self.running: self.bus.log('Already serving on %s' % self.description) return self.interrupt = None if not self.httpserver: raise ValueError('No HTTP server has been created.') if not os.environ.get('LISTEN_PID', None): # Start the httpserver in a new thread. if isinstance(self.bind_addr, tuple): portend.free(*self.bind_addr, timeout=Timeouts.free) import threading t = threading.Thread(target=self._start_http_thread) t.name = 'HTTPServer ' + t.name t.start() self.wait() self.running = True self.bus.log('Serving on %s' % self.description) start.priority = 75 @property def description(self): """ A description about where this server is bound. """ if self.bind_addr is None: on_what = 'unknown interface (dynamic?)' elif isinstance(self.bind_addr, tuple): on_what = self._get_base() else: on_what = 'socket file: %s' % self.bind_addr return on_what def _get_base(self): if not self.httpserver: return '' host, port = self.bound_addr if getattr(self.httpserver, 'ssl_adapter', None): scheme = 'https' if port != 443: host += ':%s' % port else: scheme = 'http' if port != 80: host += ':%s' % port return '%s://%s' % (scheme, host) def _start_http_thread(self): """HTTP servers MUST be running in new threads, so that the main thread persists to receive KeyboardInterrupt's. If an exception is raised in the httpserver's thread then it's trapped here, and the bus (and therefore our httpserver) are shut down. """ try: self.httpserver.start() except KeyboardInterrupt: self.bus.log(' hit: shutting down HTTP server') self.interrupt = sys.exc_info()[1] self.bus.exit() except SystemExit: self.bus.log('SystemExit raised: shutting down HTTP server') self.interrupt = sys.exc_info()[1] self.bus.exit() raise except Exception: self.interrupt = sys.exc_info()[1] self.bus.log('Error in HTTP server: shutting down', traceback=True, level=40) self.bus.exit() raise def wait(self): """Wait until the HTTP server is ready to receive requests.""" while not getattr(self.httpserver, 'ready', False): if self.interrupt: raise self.interrupt time.sleep(.1) # bypass check when LISTEN_PID is set if os.environ.get('LISTEN_PID', None): return # bypass check when running via socket-activation # (for socket-activation the port will be managed by systemd) if not isinstance(self.bind_addr, tuple): return # wait for port to be occupied with _safe_wait(*self.bound_addr): portend.occupied(*self.bound_addr, timeout=Timeouts.occupied) @property def bound_addr(self): """ The bind address, or if it's an ephemeral port and the socket has been bound, return the actual port bound. """ host, port = self.bind_addr if port == 0 and self.httpserver.socket: # Bound to ephemeral port. Get the actual port allocated. port = self.httpserver.socket.getsockname()[1] return host, port def stop(self): """Stop the HTTP server.""" if self.running: # stop() MUST block until the server is *truly* stopped. self.httpserver.stop() # Wait for the socket to be truly freed. if isinstance(self.bind_addr, tuple): portend.free(*self.bound_addr, timeout=Timeouts.free) self.running = False self.bus.log('HTTP Server %s shut down' % self.httpserver) else: self.bus.log('HTTP Server %s already shut down' % self.httpserver) stop.priority = 25 def restart(self): """Restart the HTTP server.""" self.stop() self.start() class FlupCGIServer(object): """Adapter for a flup.server.cgi.WSGIServer.""" def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs self.ready = False def start(self): """Start the CGI server.""" # We have to instantiate the server class here because its __init__ # starts a threadpool. If we do it too early, daemonize won't work. from flup.server.cgi import WSGIServer self.cgiserver = WSGIServer(*self.args, **self.kwargs) self.ready = True self.cgiserver.run() def stop(self): """Stop the HTTP server.""" self.ready = False class FlupFCGIServer(object): """Adapter for a flup.server.fcgi.WSGIServer.""" def __init__(self, *args, **kwargs): if kwargs.get('bindAddress', None) is None: import socket if not hasattr(socket, 'fromfd'): raise ValueError( 'Dynamic FCGI server not available on this platform. ' 'You must use a static or external one by providing a ' 'legal bindAddress.') self.args = args self.kwargs = kwargs self.ready = False def start(self): """Start the FCGI server.""" # We have to instantiate the server class here because its __init__ # starts a threadpool. If we do it too early, daemonize won't work. from flup.server.fcgi import WSGIServer self.fcgiserver = WSGIServer(*self.args, **self.kwargs) # TODO: report this bug upstream to flup. # If we don't set _oldSIGs on Windows, we get: # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", # line 108, in run # self._restoreSignalHandlers() # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", # line 156, in _restoreSignalHandlers # for signum,handler in self._oldSIGs: # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' self.fcgiserver._installSignalHandlers = lambda: None self.fcgiserver._oldSIGs = [] self.ready = True self.fcgiserver.run() def stop(self): """Stop the HTTP server.""" # Forcibly stop the fcgi server main event loop. self.fcgiserver._keepGoing = False # Force all worker threads to die off. self.fcgiserver._threadPool.maxSpare = ( self.fcgiserver._threadPool._idleCount) self.ready = False class FlupSCGIServer(object): """Adapter for a flup.server.scgi.WSGIServer.""" def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs self.ready = False def start(self): """Start the SCGI server.""" # We have to instantiate the server class here because its __init__ # starts a threadpool. If we do it too early, daemonize won't work. from flup.server.scgi import WSGIServer self.scgiserver = WSGIServer(*self.args, **self.kwargs) # TODO: report this bug upstream to flup. # If we don't set _oldSIGs on Windows, we get: # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", # line 108, in run # self._restoreSignalHandlers() # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", # line 156, in _restoreSignalHandlers # for signum,handler in self._oldSIGs: # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' self.scgiserver._installSignalHandlers = lambda: None self.scgiserver._oldSIGs = [] self.ready = True self.scgiserver.run() def stop(self): """Stop the HTTP server.""" self.ready = False # Forcibly stop the scgi server main event loop. self.scgiserver._keepGoing = False # Force all worker threads to die off. self.scgiserver._threadPool.maxSpare = 0 @contextlib.contextmanager def _safe_wait(host, port): """ On systems where a loopback interface is not available and the server is bound to all interfaces, it's difficult to determine whether the server is in fact occupying the port. In this case, just issue a warning and move on. See issue #1100. """ try: yield except portend.Timeout: if host == portend.client_host(host): raise msg = 'Unable to verify that the server is bound on %r' % port warnings.warn(msg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/process/win32.py0000644252176402575230000001323314307416152021074 0ustar00jaracoprimarygroup"""Windows service. Requires pywin32.""" import os import win32api import win32con import win32event import win32service import win32serviceutil from cherrypy.process import wspbus, plugins class ConsoleCtrlHandler(plugins.SimplePlugin): """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" def __init__(self, bus): self.is_set = False plugins.SimplePlugin.__init__(self, bus) def start(self): if self.is_set: self.bus.log('Handler for console events already set.', level=20) return result = win32api.SetConsoleCtrlHandler(self.handle, 1) if result == 0: self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % win32api.GetLastError(), level=40) else: self.bus.log('Set handler for console events.', level=20) self.is_set = True def stop(self): if not self.is_set: self.bus.log('Handler for console events already off.', level=20) return try: result = win32api.SetConsoleCtrlHandler(self.handle, 0) except ValueError: # "ValueError: The object has not been registered" result = 1 if result == 0: self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % win32api.GetLastError(), level=40) else: self.bus.log('Removed handler for console events.', level=20) self.is_set = False def handle(self, event): """Handle console control events (like Ctrl-C).""" if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, win32con.CTRL_CLOSE_EVENT): self.bus.log('Console event %s: shutting down bus' % event) # Remove self immediately so repeated Ctrl-C doesn't re-call it. try: self.stop() except ValueError: pass self.bus.exit() # 'First to return True stops the calls' return 1 return 0 class Win32Bus(wspbus.Bus): """A Web Site Process Bus implementation for Win32. Instead of time.sleep, this bus blocks using native win32event objects. """ def __init__(self): self.events = {} wspbus.Bus.__init__(self) def _get_state_event(self, state): """Return a win32event for the given state (creating it if needed).""" try: return self.events[state] except KeyError: event = win32event.CreateEvent(None, 0, 0, 'WSPBus %s Event (pid=%r)' % (state.name, os.getpid())) self.events[state] = event return event @property def state(self): return self._state @state.setter def state(self, value): self._state = value event = self._get_state_event(value) win32event.PulseEvent(event) def wait(self, state, interval=0.1, channel=None): """Wait for the given state(s), KeyboardInterrupt or SystemExit. Since this class uses native win32event objects, the interval argument is ignored. """ if isinstance(state, (tuple, list)): # Don't wait for an event that beat us to the punch ;) if self.state not in state: events = tuple([self._get_state_event(s) for s in state]) win32event.WaitForMultipleObjects( events, 0, win32event.INFINITE) else: # Don't wait for an event that beat us to the punch ;) if self.state != state: event = self._get_state_event(state) win32event.WaitForSingleObject(event, win32event.INFINITE) class _ControlCodes(dict): """Control codes used to "signal" a service via ControlService. User-defined control codes are in the range 128-255. We generally use the standard Python value for the Linux signal and add 128. Example: >>> signal.SIGUSR1 10 control_codes['graceful'] = 128 + 10 """ def key_for(self, obj): """For the given value, return its corresponding key.""" for key, val in self.items(): if val is obj: return key raise ValueError('The given object could not be found: %r' % obj) control_codes = _ControlCodes({'graceful': 138}) def signal_child(service, command): if command == 'stop': win32serviceutil.StopService(service) elif command == 'restart': win32serviceutil.RestartService(service) else: win32serviceutil.ControlService(service, control_codes[command]) class PyWebService(win32serviceutil.ServiceFramework): """Python Web Service.""" _svc_name_ = 'Python Web Service' _svc_display_name_ = 'Python Web Service' _svc_deps_ = None # sequence of service names on which this depends _exe_name_ = 'pywebsvc' _exe_args_ = None # Default to no arguments # Only exists on Windows 2000 or later, ignored on windows NT _svc_description_ = 'Python Web Service' def SvcDoRun(self): from cherrypy import process process.bus.start() process.bus.block() def SvcStop(self): from cherrypy import process self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) process.bus.exit() def SvcOther(self, control): from cherrypy import process process.bus.publish(control_codes.key_for(control)) if __name__ == '__main__': win32serviceutil.HandleCommandLine(PyWebService) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/cherrypy/process/wspbus.py0000644252176402575230000005172314424762003021462 0ustar00jaracoprimarygroupr"""An implementation of the Web Site Process Bus. This module is completely standalone, depending only on the stdlib. Web Site Process Bus -------------------- A Bus object is used to contain and manage site-wide behavior: daemonization, HTTP server start/stop, process reload, signal handling, drop privileges, PID file management, logging for all of these, and many more. In addition, a Bus object provides a place for each web framework to register code that runs in response to site-wide events (like process start and stop), or which controls or otherwise interacts with the site-wide components mentioned above. For example, a framework which uses file-based templates would add known template filenames to an autoreload component. Ideally, a Bus object will be flexible enough to be useful in a variety of invocation scenarios: 1. The deployer starts a site from the command line via a framework-neutral deployment script; applications from multiple frameworks are mixed in a single site. Command-line arguments and configuration files are used to define site-wide components such as the HTTP server, WSGI component graph, autoreload behavior, signal handling, etc. 2. The deployer starts a site via some other process, such as Apache; applications from multiple frameworks are mixed in a single site. Autoreload and signal handling (from Python at least) are disabled. 3. The deployer starts a site via a framework-specific mechanism; for example, when running tests, exploring tutorials, or deploying single applications from a single framework. The framework controls which site-wide components are enabled as it sees fit. The Bus object in this package uses topic-based publish-subscribe messaging to accomplish all this. A few topic channels are built in ('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and site containers are free to define their own. If a message is sent to a channel that has not been defined or has no listeners, there is no effect. In general, there should only ever be a single Bus object per process. Frameworks and site containers share a single Bus object by publishing messages and subscribing listeners. The Bus object works as a finite state machine which models the current state of the process. Bus methods move it from one state to another; those methods then publish to subscribed listeners on the channel for the new state.:: O | V STOPPING --> STOPPED --> EXITING -> X A A | | \___ | | \ | | V V STARTED <-- STARTING """ import atexit try: import ctypes except ImportError: """Google AppEngine is shipped without ctypes :seealso: http://stackoverflow.com/a/6523777/70170 """ ctypes = None import operator import os import sys import threading import time import traceback as _traceback import warnings import subprocess import functools from more_itertools import always_iterable # Here I save the value of os.getcwd(), which, if I am imported early enough, # will be the directory from which the startup script was run. This is needed # by _do_execv(), to change back to the original directory before execv()ing a # new process. This is a defense against the application having changed the # current working directory (which could make sys.executable "not found" if # sys.executable is a relative-path, and/or cause other problems). _startup_cwd = os.getcwd() class ChannelFailures(Exception): """Exception raised during errors on Bus.publish().""" delimiter = '\n' def __init__(self, *args, **kwargs): """Initialize ChannelFailures errors wrapper.""" super(ChannelFailures, self).__init__(*args, **kwargs) self._exceptions = list() def handle_exception(self): """Append the current exception to self.""" self._exceptions.append(sys.exc_info()[1]) def get_instances(self): """Return a list of seen exception instances.""" return self._exceptions[:] def __str__(self): """Render the list of errors, which happened in channel.""" exception_strings = map(repr, self.get_instances()) return self.delimiter.join(exception_strings) __repr__ = __str__ def __bool__(self): """Determine whether any error happened in channel.""" return bool(self._exceptions) __nonzero__ = __bool__ # Use a flag to indicate the state of the bus. class _StateEnum(object): class State(object): name = None def __repr__(self): return 'states.%s' % self.name def __setattr__(self, key, value): if isinstance(value, self.State): value.name = key object.__setattr__(self, key, value) states = _StateEnum() states.STOPPED = states.State() states.STARTING = states.State() states.STARTED = states.State() states.STOPPING = states.State() states.EXITING = states.State() try: import fcntl except ImportError: max_files = 0 else: try: max_files = os.sysconf('SC_OPEN_MAX') except AttributeError: max_files = 1024 class Bus(object): """Process state-machine and messenger for HTTP site deployment. All listeners for a given channel are guaranteed to be called even if others at the same channel fail. Each failure is logged, but execution proceeds on to the next listener. The only way to stop all processing from inside a listener is to raise SystemExit and stop the whole server. """ states = states state = states.STOPPED execv = False max_cloexec_files = max_files def __init__(self): """Initialize pub/sub bus.""" self.execv = False self.state = states.STOPPED channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main' self.listeners = dict( (channel, set()) for channel in channels ) self._priorities = {} def subscribe(self, channel, callback=None, priority=None): """Add the given callback at the given channel (if not present). If callback is None, return a partial suitable for decorating the callback. """ if callback is None: return functools.partial( self.subscribe, channel, priority=priority, ) ch_listeners = self.listeners.setdefault(channel, set()) ch_listeners.add(callback) if priority is None: priority = getattr(callback, 'priority', 50) self._priorities[(channel, callback)] = priority def unsubscribe(self, channel, callback): """Discard the given callback (if present).""" listeners = self.listeners.get(channel) if listeners and callback in listeners: listeners.discard(callback) del self._priorities[(channel, callback)] def publish(self, channel, *args, **kwargs): """Return output of all subscribers for the given channel.""" if channel not in self.listeners: return [] exc = ChannelFailures() output = [] raw_items = ( (self._priorities[(channel, listener)], listener) for listener in self.listeners[channel] ) items = sorted(raw_items, key=operator.itemgetter(0)) for priority, listener in items: try: output.append(listener(*args, **kwargs)) except KeyboardInterrupt: raise except SystemExit: e = sys.exc_info()[1] # If we have previous errors ensure the exit code is non-zero if exc and e.code == 0: e.code = 1 raise except Exception: exc.handle_exception() if channel == 'log': # Assume any further messages to 'log' will fail. pass else: self.log('Error in %r listener %r' % (channel, listener), level=40, traceback=True) if exc: raise exc return output def _clean_exit(self): """Assert that the Bus is not running in atexit handler callback.""" if self.state != states.EXITING: warnings.warn( 'The main thread is exiting, but the Bus is in the %r state; ' 'shutting it down automatically now. You must either call ' 'bus.block() after start(), or call bus.exit() before the ' 'main thread exits.' % self.state, RuntimeWarning) self.exit() def start(self): """Start all services.""" atexit.register(self._clean_exit) self.state = states.STARTING self.log('Bus STARTING') try: self.publish('start') self.state = states.STARTED self.log('Bus STARTED') except (KeyboardInterrupt, SystemExit): raise except Exception: self.log('Shutting down due to error in start listener:', level=40, traceback=True) e_info = sys.exc_info()[1] try: self.exit() except Exception: # Any stop/exit errors will be logged inside publish(). pass # Re-raise the original error raise e_info def exit(self): """Stop all services and prepare to exit the process.""" exitstate = self.state EX_SOFTWARE = 70 try: self.stop() self.state = states.EXITING self.log('Bus EXITING') self.publish('exit') # This isn't strictly necessary, but it's better than seeing # "Waiting for child threads to terminate..." and then nothing. self.log('Bus EXITED') except Exception: # This method is often called asynchronously (whether thread, # signal handler, console handler, or atexit handler), so we # can't just let exceptions propagate out unhandled. # Assume it's been logged and just die. os._exit(EX_SOFTWARE) if exitstate == states.STARTING: # exit() was called before start() finished, possibly due to # Ctrl-C because a start listener got stuck. In this case, # we could get stuck in a loop where Ctrl-C never exits the # process, so we just call os.exit here. os._exit(EX_SOFTWARE) def restart(self): """Restart the process (may close connections). This method does not restart the process from the calling thread; instead, it stops the bus and asks the main thread to call execv. """ self.execv = True self.exit() def graceful(self): """Advise all services to reload.""" self.log('Bus graceful') self.publish('graceful') def block(self, interval=0.1): """Wait for the EXITING state, KeyboardInterrupt or SystemExit. This function is intended to be called only by the main thread. After waiting for the EXITING state, it also waits for all threads to terminate, and then calls os.execv if self.execv is True. This design allows another thread to call bus.restart, yet have the main thread perform the actual execv call (required on some platforms). """ try: self.wait(states.EXITING, interval=interval, channel='main') except (KeyboardInterrupt, IOError): # The time.sleep call might raise # "IOError: [Errno 4] Interrupted function call" on KBInt. self.log('Keyboard Interrupt: shutting down bus') self.exit() except SystemExit: self.log('SystemExit raised: shutting down bus') self.exit() raise # Waiting for ALL child threads to finish is necessary on OS X. # See https://github.com/cherrypy/cherrypy/issues/581. # It's also good to let them all shut down before allowing # the main thread to call atexit handlers. # See https://github.com/cherrypy/cherrypy/issues/751. self.log('Waiting for child threads to terminate...') for t in threading.enumerate(): # Validate the we're not trying to join the MainThread # that will cause a deadlock and the case exist when # implemented as a windows service and in any other case # that another thread executes cherrypy.engine.exit() if ( t != threading.current_thread() and not isinstance(t, threading._MainThread) and # Note that any dummy (external) threads are # always daemonic. not t.daemon ): self.log('Waiting for thread %s.' % t.name) t.join() if self.execv: self._do_execv() def wait(self, state, interval=0.1, channel=None): """Poll for the given state(s) at intervals; publish to channel.""" states = set(always_iterable(state)) while self.state not in states: time.sleep(interval) self.publish(channel) def _do_execv(self): """Re-execute the current process. This must be called from the main thread, because certain platforms (OS X) don't allow execv to be called in a child thread very well. """ try: args = self._get_true_argv() except NotImplementedError: """It's probably win32 or GAE""" args = [sys.executable] + self._get_interpreter_argv() + sys.argv self.log('Re-spawning %s' % ' '.join(args)) self._extend_pythonpath(os.environ) if sys.platform[:4] == 'java': from _systemrestart import SystemRestart raise SystemRestart else: if sys.platform == 'win32': args = ['"%s"' % arg for arg in args] os.chdir(_startup_cwd) if self.max_cloexec_files: self._set_cloexec() os.execv(sys.executable, args) @staticmethod def _get_interpreter_argv(): """Retrieve current Python interpreter's arguments. Returns empty tuple in case of frozen mode, uses built-in arguments reproduction function otherwise. Frozen mode is possible for the app has been packaged into a binary executable using py2exe. In this case the interpreter's arguments are already built-in into that executable. :seealso: https://github.com/cherrypy/cherrypy/issues/1526 Ref: https://pythonhosted.org/PyInstaller/runtime-information.html """ return ([] if getattr(sys, 'frozen', False) else subprocess._args_from_interpreter_flags()) @staticmethod def _get_true_argv(): """Retrieve all real arguments of the python interpreter. ...even those not listed in ``sys.argv`` :seealso: http://stackoverflow.com/a/28338254/595220 :seealso: http://stackoverflow.com/a/6683222/595220 :seealso: http://stackoverflow.com/a/28414807/595220 """ try: char_p = ctypes.c_wchar_p argv = ctypes.POINTER(char_p)() argc = ctypes.c_int() ctypes.pythonapi.Py_GetArgcArgv( ctypes.byref(argc), ctypes.byref(argv), ) _argv = argv[:argc.value] # The code below is trying to correctly handle special cases. # `-c`'s argument interpreted by Python itself becomes `-c` as # well. Same applies to `-m`. This snippet is trying to survive # at least the case with `-m` # Ref: https://github.com/cherrypy/cherrypy/issues/1545 # Ref: python/cpython@418baf9 argv_len, is_command, is_module = len(_argv), False, False try: m_ind = _argv.index('-m') if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'): """ In some older Python versions `-m`'s argument may be substituted with `-c`, not `-m` """ is_module = True except (IndexError, ValueError): m_ind = None try: c_ind = _argv.index('-c') if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c': is_command = True except (IndexError, ValueError): c_ind = None if is_module: """It's containing `-m -m` sequence of arguments""" if is_command and c_ind < m_ind: """There's `-c -c` before `-m`""" raise RuntimeError( "Cannot reconstruct command from '-c'. Ref: " 'https://github.com/cherrypy/cherrypy/issues/1545') # Survive module argument here original_module = sys.argv[0] if not os.access(original_module, os.R_OK): """There's no such module exist""" raise AttributeError( "{} doesn't seem to be a module " 'accessible by current user'.format(original_module)) del _argv[m_ind:m_ind + 2] # remove `-m -m` # ... and substitute it with the original module path: _argv.insert(m_ind, original_module) elif is_command: """It's containing just `-c -c` sequence of arguments""" raise RuntimeError( "Cannot reconstruct command from '-c'. " 'Ref: https://github.com/cherrypy/cherrypy/issues/1545') except AttributeError: """It looks Py_GetArgcArgv's completely absent in some environments It is known, that there's no Py_GetArgcArgv in MS Windows and ``ctypes`` module is completely absent in Google AppEngine :seealso: https://github.com/cherrypy/cherrypy/issues/1506 :seealso: https://github.com/cherrypy/cherrypy/issues/1512 :ref: http://bit.ly/2gK6bXK """ raise NotImplementedError else: return _argv @staticmethod def _extend_pythonpath(env): """Prepend current working dir to PATH environment variable if needed. If sys.path[0] is an empty string, the interpreter was likely invoked with -m and the effective path is about to change on re-exec. Add the current directory to $PYTHONPATH to ensure that the new process sees the same path. This issue cannot be addressed in the general case because Python cannot reliably reconstruct the original command line (http://bugs.python.org/issue14208). (This idea filched from tornado.autoreload) """ path_prefix = '.' + os.pathsep existing_path = env.get('PYTHONPATH', '') needs_patch = ( sys.path[0] == '' and not existing_path.startswith(path_prefix) ) if needs_patch: env['PYTHONPATH'] = path_prefix + existing_path def _set_cloexec(self): """Set the CLOEXEC flag on all open files (except stdin/out/err). If self.max_cloexec_files is an integer (the default), then on platforms which support it, it represents the max open files setting for the operating system. This function will be called just before the process is restarted via os.execv() to prevent open files from persisting into the new process. Set self.max_cloexec_files to 0 to disable this behavior. """ for fd in range(3, self.max_cloexec_files): # skip stdin/out/err try: flags = fcntl.fcntl(fd, fcntl.F_GETFD) except IOError: continue fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) def stop(self): """Stop all services.""" self.state = states.STOPPING self.log('Bus STOPPING') self.publish('stop') self.state = states.STOPPED self.log('Bus STOPPED') def start_with_callback(self, func, args=None, kwargs=None): """Start 'func' in a new thread T, then start self (and return T).""" if args is None: args = () if kwargs is None: kwargs = {} args = (func,) + args def _callback(func, *a, **kw): self.wait(states.STARTED) func(*a, **kw) t = threading.Thread(target=_callback, args=args, kwargs=kwargs) t.name = 'Bus Callback ' + t.name t.start() self.start() return t def log(self, msg='', level=20, traceback=False): """Log the given message. Append the last traceback if requested.""" if traceback: msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info())) self.publish('log', msg, level) bus = Bus() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1702477980.529389 CherryPy-18.9.0/cherrypy/scaffold/0000755252176402575230000000000014536340235017663 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/scaffold/__init__.py0000644252176402575230000000371514307416152022000 0ustar00jaracoprimarygroup""", a CherryPy application. Use this as a base for creating new CherryPy applications. When you want to make a new app, copy and paste this folder to some other location (maybe site-packages) and rename it to the name of your project, then tweak as desired. Even before any tweaking, this should serve a few demonstration pages. Change to this directory and run: cherryd -c site.conf """ import cherrypy from cherrypy import tools, url import os local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) @cherrypy.config(**{'tools.log_tracebacks.on': True}) class Root: """Declaration of the CherryPy app URI structure.""" @cherrypy.expose def index(self): """Render HTML-template at the root path of the web-app.""" return """ Try some other path, or a default path.
Or, just look at the pretty picture:
""" % (url('other'), url('else'), url('files/made_with_cherrypy_small.png')) @cherrypy.expose def default(self, *args, **kwargs): """Render catch-all args and kwargs.""" return 'args: %s kwargs: %s' % (args, kwargs) @cherrypy.expose def other(self, a=2, b='bananas', c=None): """Render number of fruits based on third argument.""" cherrypy.response.headers['Content-Type'] = 'text/plain' if c is None: return 'Have %d %s.' % (int(a), b) else: return 'Have %d %s, %s.' % (int(a), b, c) files = tools.staticdir.handler( section='/files', dir=os.path.join(local_dir, 'static'), # Ignore .php files, etc. match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', ) root = Root() # Uncomment the following to use your own favicon instead of CP's default. # favicon_path = os.path.join(local_dir, "favicon.ico") # root.favicon_ico = tools.staticfile.handler(filename=favicon_path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/scaffold/apache-fcgi.conf0000644252176402575230000000164214307416152022662 0ustar00jaracoprimarygroup# Apache2 server conf file for using CherryPy with mod_fcgid. # This doesn't have to be "C:/", but it has to be a directory somewhere, and # MUST match the directory used in the FastCgiExternalServer directive, below. DocumentRoot "C:/" ServerName 127.0.0.1 Listen 80 LoadModule fastcgi_module modules/mod_fastcgi.dll LoadModule rewrite_module modules/mod_rewrite.so Options ExecCGI SetHandler fastcgi-script RewriteEngine On # Send requests for any URI to our fastcgi handler. RewriteRule ^(.*)$ /fastcgi.pyc [L] # The FastCgiExternalServer directive defines filename as an external FastCGI application. # If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. # The filename does not have to exist in the local filesystem. URIs that Apache resolves to this # filename will be handled by this external FastCGI application. FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/scaffold/example.conf0000644252176402575230000000007614307416152022166 0ustar00jaracoprimarygroup[/] log.error_file: "error.log" log.access_file: "access.log" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/scaffold/site.conf0000644252176402575230000000065214307416152021477 0ustar00jaracoprimarygroup[global] # Uncomment this when you're done developing #environment: "production" server.socket_host: "0.0.0.0" server.socket_port: 8088 # Uncomment the following lines to run on HTTPS at the same time #server.2.socket_host: "0.0.0.0" #server.2.socket_port: 8433 #server.2.ssl_certificate: '../test/test.pem' #server.2.ssl_private_key: '../test/test.pem' tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5296075 CherryPy-18.9.0/cherrypy/scaffold/static/0000755252176402575230000000000014536340235021152 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/scaffold/static/made_with_cherrypy_small.png0000644252176402575230000001431314307416152026736 0ustar00jaracoprimarygroupPNG  IHDR3fIDATx|XۻEݭk'Jt34HwҠtwIt C?3<>~s_gdd-, N}AOT.R]9=& EF΃rԯoc4MHke1`GMCBhIG4%%Ã8JY9^]MqŧӇdZBKN|1سͭ\L= ccZZ}APsЙ\뮞&II`i GꪎJ7p <'H/e02UI] )`޿R‘_G>4 *о!=/VT UzKf,h@t h`D9-eȇ?Y‘#0}m)J0&ɵ#3 ueU9`X )έAY\;OS8ON&ĪORťY__s@ݟ&peM3ťVpcpoҔ'D -p:5^^'&vqivUH"ʡ:|^w GFCݹAC< D#{ u0:Mrx҄ 3:uW50_`vQmFQFjwqScN{[y%~5A!@¦W͈ )JX]m (|JcaXd Ƽmx3^^fp2lR̯TYZfWH1R(e"}k9_s6Vlu6sCuzd5 YVw-"!^^j=Ur#cbw.@rwm0V+Μ ##CO¡ធp:YW44E 08wM=ՖlvIe$w}k,`[m]cΔQerlCݾb5^V8=a+͛SZUar&~H1q 4|,>,!2`Gdbm%0cK5$G~/݆kuFLެ^$'  1؞zmphbH=QsWgy'lZ3tn."(2s7FY3Q[x*C(OA^D^Bl$7q`K-n1o,pCt+{לrT On5Yj*֛u:fJ$`WaV>?KXUh*Q=X`NnGåa!v+ɗ[1H&!V<(/. X<y]KbkueDn'68WL;C@+))Z.jHH1\qΏ# ΎՊ1VSdE1Ž= 5ᗗ4i~2Q6(A=Xg OѰtQL㣄 6RoTB4 )‚,˴V]RB7wx_KbgMibmzJBjC*Y,V-y ~ 1w6Wd@1~7DhU]h'@Әi b˴A]@Som} e΁h]G@*ر0r㈂ETsy]&b#gsjW;`aI5~Z U쯲4pz_(R?4.RL[)b{bE!ZR1~)ΜPVB5Fc=K$ ys,}¯0޷O\؇QMDK=em5A3^9?JՉ 2݊E? AH1*d^R| >7.Wg>)4%)̰Py^1'cERl;^TIrcI=o]k2N-y#| HU.z} @za#Jj:dT+c4W '6w5 4ܔv1g??ΩgN|na6o)LOhi< Chz5̂lwl4.,쩩WŭLGuX/x<&j.US,'[>I4O0KГI2M+mFkN93>WJ&2uZl<6\_-nnٳg ~7h:ڽ={vJR+VΝ;wѢ)2߿/wTRRfyt Hj]=pR Z RZ9;T?a__NaW[iΩ:fD8C6v;}\$ܲy#OR;<17|MX!6#g\z/VXA Tụ:-.Yu/]8tI*.fЙG/[v[gwlo)5s ==}͛saC r`4HlL<36n >}tt-|.W P墜vYƁ\jYB56^KbdЏ`_(:*lzFQ0>ѷz\=d` e=Pe"/AsF$zh39 S]lc{BrP Yv,C#-]2gnͪ%nshƋ6JU>z:wo =dT+- :$>LmR;5šZ׌;[c^`"onY6J bHJ37nΥE N{8s6uTXh$Z9brq3-23}QɎ:Oޝ࿂?Na4V &0ŀՂܵ)1oc9HGNULkz^0f]́Gz _Q=K r AM,!7kIث ᆢ lBWhTqW}fDDx3dxhTc@1Cz|s7쁐okG<.6ҋ-3alJܒqk`^~ϵB$InH?@7Nf 'O-Sd[Y DŽ\bLx5@<[zGJkQs <9uUPdvoc yڳS;ӲeKYd1\3+&XmcTsw@B=uW[S ;cq* m6-%N͟E.s#W9/SbLug+{&aDmƃ+ i6M.6j(a[v.15`Aڽ:otf ![*QA2o16Pd6'DsKg;(j<> 9N|.ٲ4-t$;J];h.@kX]<{TB㛓slTB{E(@-ZD8-[6I\?S8F0N9xGtك1O&Ïy%˺p1Q&'iZA¡uS~k鮶+& C f 0kV}S]fJ4?|@ Kܹs>JhEk -s6 m(qQ dc"6a.Q (p{vz30F `}0]TR]~Z19:-v9pDSpd2$gȎI87|+8wrs]d[nKHlfTU/gvd>Y6ى#g uR,įdュ G^ꢈ@wsZ_a!- Bޔ(6e wP]$*wq闱qSk.k,]4)DBjDӳ7p.upblC%hc\ [fg*ƒD2l~DU (v ;ɦ3" "=F%tǒdr.Y\u:o.:OMbBO }dХ{Wq5N65IyIpiM,zmm\7Ք#rsîtRLXGpJދkJvd 15:*&n%c 2w6E(jw8~to+)pw}L&K6љ2<]v SSi"kڛ_pD _ eMH7؆߿~d7^`;J0uwX'IDK8¿?7cQۄWs}9u\\swlKcf]H3 5*ul\ =o$lܻBWؼ8pVv5^m⹒/t|ۻihԷOrEEeښ lK߼%r3jYSaɷ9  YjadgP6m^굟 d{^nNAB;AߓqmGϽ˗YHN5j%9]:2pBYBjjgGЈS_73Q&Ijm?v$|,< Sd6m- b`ts?bA$s+csB`r.ie)Օi>}tdO?yNOwj2(dK= شhj؊8?OD{*U)ߩPK1om|vks$HSf(;II'^$A-V'P%$NhPQeaeJ|(|q.8Ō9~$ޖHqta,( 33<<`o39븊W/㌌ ߾00ppZgZgZ? i1 [7IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5417244 CherryPy-18.9.0/cherrypy/test/0000755252176402575230000000000014536340235017061 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/__init__.py0000644252176402575230000000061414307416152021171 0ustar00jaracoprimarygroup""" Regression test suite for CherryPy. """ import os import sys def newexit(): os._exit(1) def setup(): # We want to monkey patch sys.exit so that we can get some # information about where exit is being called. newexit._old = sys.exit sys.exit = newexit def teardown(): try: sys.exit = sys.exit._old except AttributeError: sys.exit = sys._exit ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/_test_decorators.py0000644252176402575230000000166714307416152023006 0ustar00jaracoprimarygroup"""Test module for the @-decorator syntax, which is version-specific""" import cherrypy from cherrypy import expose, tools class ExposeExamples(object): @expose def no_call(self): return 'Mr E. R. Bradshaw' @expose() def call_empty(self): return 'Mrs. B.J. Smegma' @expose('call_alias') def nesbitt(self): return 'Mr Nesbitt' @expose(['alias1', 'alias2']) def andrews(self): return 'Mr Ken Andrews' @expose(alias='alias3') def watson(self): return 'Mr. and Mrs. Watson' class ToolExamples(object): @expose # This is here to demonstrate that using the config decorator # does not overwrite other config attributes added by the Tool # decorator (in this case response_headers). @cherrypy.config(**{'response.stream': True}) @tools.response_headers(headers=[('Content-Type', 'application/data')]) def blah(self): yield b'blah' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/_test_states_demo.py0000644252176402575230000000352414307416152023142 0ustar00jaracoprimarygroupimport os import sys import time import cherrypy starttime = time.time() class Root: @cherrypy.expose def index(self): return 'Hello World' @cherrypy.expose def mtimes(self): return repr(cherrypy.engine.publish('Autoreloader', 'mtimes')) @cherrypy.expose def pid(self): return str(os.getpid()) @cherrypy.expose def start(self): return repr(starttime) @cherrypy.expose def exit(self): # This handler might be called before the engine is STARTED if an # HTTP worker thread handles it before the HTTP server returns # control to engine.start. We avoid that race condition here # by waiting for the Bus to be STARTED. cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) cherrypy.engine.exit() @cherrypy.engine.subscribe('start', priority=100) def unsub_sig(): cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False)) if cherrypy.config.get('unsubsig', False): cherrypy.log('Unsubscribing the default cherrypy signal handler') cherrypy.engine.signal_handler.unsubscribe() try: from signal import signal, SIGTERM except ImportError: pass else: def old_term_handler(signum=None, frame=None): cherrypy.log('I am an old SIGTERM handler.') sys.exit(0) cherrypy.log('Subscribing the new one.') signal(SIGTERM, old_term_handler) @cherrypy.engine.subscribe('start', priority=6) def starterror(): if cherrypy.config.get('starterror', False): 1 / 0 @cherrypy.engine.subscribe('start', priority=6) def log_test_case_name(): if cherrypy.config.get('test_case_name', False): cherrypy.log('STARTED FROM: %s' % cherrypy.config.get('test_case_name')) cherrypy.tree.mount(Root(), '/', {'/': {}}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/benchmark.py0000644252176402575230000003042314307416152021365 0ustar00jaracoprimarygroup"""CherryPy Benchmark Tool Usage: benchmark.py [options] --null: use a null Request object (to bench the HTTP server only) --notests: start the server but do not run the tests; this allows you to check the tested pages with a browser --help: show this help message --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) --modpython: run tests via apache on 54583 (with modpython_gateway) --ab=path: Use the ab script/executable at 'path' (see below) --apache=path: Use the apache script/exe at 'path' (see below) To run the benchmarks, the Apache Benchmark tool "ab" must either be on your system path, or specified via the --ab=path option. To run the modpython tests, the "apache" executable or script must be on your system path, or provided via the --apache=path option. On some platforms, "apache" may be called "apachectl" or "apache2ctl"--create a symlink to them if needed. """ import getopt import os import re import sys import time import cherrypy from cherrypy import _cperror, _cpmodpy from cherrypy.lib import httputil curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) AB_PATH = '' APACHE_PATH = 'apache' SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog' __all__ = ['ABSession', 'Root', 'print_report', 'run_standard_benchmarks', 'safe_threads', 'size_report', 'thread_report', ] size_cache = {} class Root: @cherrypy.expose def index(self): return """ CherryPy Benchmark """ @cherrypy.expose def hello(self): return 'Hello, world\r\n' @cherrypy.expose def sizer(self, size): resp = size_cache.get(size, None) if resp is None: size_cache[size] = resp = 'X' * int(size) return resp def init(): cherrypy.config.update({ 'log.error.file': '', 'environment': 'production', 'server.socket_host': '127.0.0.1', 'server.socket_port': 54583, 'server.max_request_header_size': 0, 'server.max_request_body_size': 0, }) # Cheat mode on ;) del cherrypy.config['tools.log_tracebacks.on'] del cherrypy.config['tools.log_headers.on'] del cherrypy.config['tools.trailing_slash.on'] appconf = { '/static': { 'tools.staticdir.on': True, 'tools.staticdir.dir': 'static', 'tools.staticdir.root': curdir, }, } globals().update( app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf), ) class NullRequest: """A null HTTP request class, returning 200 and an empty body.""" def __init__(self, local, remote, scheme='http'): pass def close(self): pass def run(self, method, path, query_string, protocol, headers, rfile): cherrypy.response.status = '200 OK' cherrypy.response.header_list = [('Content-Type', 'text/html'), ('Server', 'Null CherryPy'), ('Date', httputil.HTTPDate()), ('Content-Length', '0'), ] cherrypy.response.body = [''] return cherrypy.response class NullResponse: pass class ABSession: """A session of 'ab', the Apache HTTP server benchmarking tool. Example output from ab: This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Server Software: CherryPy/3.1beta Server Hostname: 127.0.0.1 Server Port: 54583 Document Path: /static/index.html Document Length: 14 bytes Concurrency Level: 10 Time taken for tests: 9.643867 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 189000 bytes HTML transferred: 14000 bytes Requests per second: 103.69 [#/sec] (mean) Time per request: 96.439 [ms] (mean) Time per request: 9.644 [ms] (mean, across all concurrent requests) Transfer rate: 19.08 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 2.9 0 10 Processing: 20 94 7.3 90 130 Waiting: 0 43 28.1 40 100 Total: 20 95 7.3 100 130 Percentage of the requests served within a certain time (ms) 50% 100 66% 100 75% 100 80% 100 90% 100 95% 100 98% 100 99% 110 100% 130 (longest request) Finished 1000 requests """ parse_patterns = [ ('complete_requests', 'Completed', br'^Complete requests:\s*(\d+)'), ('failed_requests', 'Failed', br'^Failed requests:\s*(\d+)'), ('requests_per_second', 'req/sec', br'^Requests per second:\s*([0-9.]+)'), ('time_per_request_concurrent', 'msec/req', br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'), ('transfer_rate', 'KB/sec', br'^Transfer rate:\s*([0-9.]+)') ] def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000, concurrency=10): self.path = path self.requests = requests self.concurrency = concurrency def args(self): port = cherrypy.server.socket_port assert self.concurrency > 0 assert self.requests > 0 # Don't use "localhost". # Cf # http://mail.python.org/pipermail/python-win32/2008-March/007050.html return ('-k -n %s -c %s http://127.0.0.1:%s%s' % (self.requests, self.concurrency, port, self.path)) def run(self): # Parse output of ab, setting attributes on self try: self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args()) except Exception: print(_cperror.format_exc()) raise for attr, name, pattern in self.parse_patterns: val = re.search(pattern, self.output, re.MULTILINE) if val: val = val.group(1) setattr(self, attr, val) else: setattr(self, attr, None) safe_threads = (25, 50, 100, 200, 400) if sys.platform in ('win32',): # For some reason, ab crashes with > 50 threads on my Win2k laptop. safe_threads = (10, 20, 30, 40, 50) def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads): sess = ABSession(path) attrs, names, patterns = list(zip(*sess.parse_patterns)) avg = dict.fromkeys(attrs, 0.0) yield ('threads',) + names for c in concurrency: sess.concurrency = c sess.run() row = [c] for attr in attrs: val = getattr(sess, attr) if val is None: print(sess.output) row = None break val = float(val) avg[attr] += float(val) row.append(val) if row: yield row # Add a row of averages. yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs] def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), concurrency=50): sess = ABSession(concurrency=concurrency) attrs, names, patterns = list(zip(*sess.parse_patterns)) yield ('bytes',) + names for sz in sizes: sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz) sess.run() yield [sz] + [getattr(sess, attr) for attr in attrs] def print_report(rows): for row in rows: print('') for val in row: sys.stdout.write(str(val).rjust(10) + ' | ') print('') def run_standard_benchmarks(): print('') print('Client Thread Report (1000 requests, 14 byte response body, ' '%s server threads):' % cherrypy.server.thread_pool) print_report(thread_report()) print('') print('Client Thread Report (1000 requests, 14 bytes via staticdir, ' '%s server threads):' % cherrypy.server.thread_pool) print_report(thread_report('%s/static/index.html' % SCRIPT_NAME)) print('') print('Size Report (1000 requests, 50 client threads, ' '%s server threads):' % cherrypy.server.thread_pool) print_report(size_report()) # modpython and other WSGI # def startup_modpython(req=None): """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI). """ if cherrypy.engine.state == cherrypy._cpengine.STOPPED: if req: if 'nullreq' in req.get_options(): cherrypy.engine.request_class = NullRequest cherrypy.engine.response_class = NullResponse ab_opt = req.get_options().get('ab', '') if ab_opt: global AB_PATH AB_PATH = ab_opt cherrypy.engine.start() if cherrypy.engine.state == cherrypy._cpengine.STARTING: cherrypy.engine.wait() return 0 # apache.OK def run_modpython(use_wsgi=False): print('Starting mod_python...') pyopts = [] # Pass the null and ab=path options through Apache if '--null' in opts: pyopts.append(('nullreq', '')) if '--ab' in opts: pyopts.append(('ab', opts['--ab'])) s = _cpmodpy.ModPythonServer if use_wsgi: pyopts.append(('wsgi.application', 'cherrypy::tree')) pyopts.append( ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython')) handler = 'modpython_gateway::handler' s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH, handler=handler) else: pyopts.append( ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython')) s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) try: s.start() run() finally: s.stop() if __name__ == '__main__': init() longopts = ['cpmodpy', 'modpython', 'null', 'notests', 'help', 'ab=', 'apache='] try: switches, args = getopt.getopt(sys.argv[1:], '', longopts) opts = dict(switches) except getopt.GetoptError: print(__doc__) sys.exit(2) if '--help' in opts: print(__doc__) sys.exit(0) if '--ab' in opts: AB_PATH = opts['--ab'] if '--notests' in opts: # Return without stopping the server, so that the pages # can be tested from a standard web browser. def run(): port = cherrypy.server.socket_port print('You may now open http://127.0.0.1:%s%s/' % (port, SCRIPT_NAME)) if '--null' in opts: print('Using null Request object') else: def run(): end = time.time() - start print('Started in %s seconds' % end) if '--null' in opts: print('\nUsing null Request object') try: try: run_standard_benchmarks() except Exception: print(_cperror.format_exc()) raise finally: cherrypy.engine.exit() print('Starting CherryPy app server...') class NullWriter(object): """Suppresses the printing of socket errors.""" def write(self, data): pass sys.stderr = NullWriter() start = time.time() if '--cpmodpy' in opts: run_modpython() elif '--modpython' in opts: run_modpython(use_wsgi=True) else: if '--null' in opts: cherrypy.server.request_class = NullRequest cherrypy.server.response_class = NullResponse cherrypy.engine.start_with_callback(run) cherrypy.engine.block() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/checkerdemo.py0000644252176402575230000000350514307416152021705 0ustar00jaracoprimarygroup"""Demonstration app for cherrypy.checker. This application is intentionally broken and badly designed. To demonstrate the output of the CherryPy Checker, simply execute this module. """ import os import cherrypy thisdir = os.path.dirname(os.path.abspath(__file__)) class Root: pass if __name__ == '__main__': conf = {'/base': {'tools.staticdir.root': thisdir, # Obsolete key. 'throw_errors': True, }, # This entry should be OK. '/base/static': {'tools.staticdir.on': True, 'tools.staticdir.dir': 'static'}, # Warn on missing folder. '/base/js': {'tools.staticdir.on': True, 'tools.staticdir.dir': 'js'}, # Warn on dir with an abs path even though we provide root. '/base/static2': {'tools.staticdir.on': True, 'tools.staticdir.dir': '/static'}, # Warn on dir with a relative path with no root. '/static3': {'tools.staticdir.on': True, 'tools.staticdir.dir': 'static'}, # Warn on unknown namespace '/unknown': {'toobles.gzip.on': True}, # Warn special on cherrypy..* '/cpknown': {'cherrypy.tools.encode.on': True}, # Warn on mismatched types '/conftype': {'request.show_tracebacks': 14}, # Warn on unknown tool. '/web': {'tools.unknown.on': True}, # Warn on server.* in app config. '/app1': {'server.socket_host': '0.0.0.0'}, # Warn on 'localhost' 'global': {'server.socket_host': 'localhost'}, # Warn on '[name]' '[/extra_brackets]': {}, } cherrypy.quickstart(Root(), config=conf) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/fastcgi.conf0000644252176402575230000000125614307416152021352 0ustar00jaracoprimarygroup # Apache2 server conf file for testing CherryPy with mod_fastcgi. # fumanchu: I had to hard-code paths due to crazy Debian layouts :( ServerRoot /usr/lib/apache2 User #1000 ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test" ServerName 127.0.0.1 Listen 8080 LoadModule fastcgi_module modules/mod_fastcgi.so LoadModule rewrite_module modules/mod_rewrite.so Options +ExecCGI SetHandler fastcgi-script RewriteEngine On RewriteRule ^(.*)$ /fastcgi.pyc [L] FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/fcgi.conf0000644252176402575230000000074614307416152020645 0ustar00jaracoprimarygroup # Apache2 server conf file for testing CherryPy with mod_fcgid. DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test" ServerName 127.0.0.1 Listen 8080 LoadModule fastcgi_module modules/mod_fastcgi.dll LoadModule rewrite_module modules/mod_rewrite.so Options ExecCGI SetHandler fastcgi-script RewriteEngine On RewriteRule ^(.*)$ /fastcgi.pyc [L] FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/helper.py0000644252176402575230000004001614307416152020711 0ustar00jaracoprimarygroup"""A library of helper functions for the CherryPy test suite.""" import datetime import io import logging import os import re import subprocess import sys import time import unittest import warnings import contextlib import portend import pytest from cheroot.test import webtest import cherrypy from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob from cherrypy.lib import httputil from cherrypy.lib import gctools log = logging.getLogger(__name__) thisdir = os.path.abspath(os.path.dirname(__file__)) serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') class Supervisor(object): """Base class for modeling and controlling servers during testing.""" def __init__(self, **kwargs): for k, v in kwargs.items(): if k == 'port': setattr(self, k, int(v)) setattr(self, k, v) def log_to_stderr(msg, level): return sys.stderr.write(msg + os.linesep) class LocalSupervisor(Supervisor): """Base class for modeling/controlling servers which run in the same process. When the server side runs in a different process, start/stop can dump all state between each test module easily. When the server side runs in the same process as the client, however, we have to do a bit more work to ensure config and mounted apps are reset between tests. """ using_apache = False using_wsgi = False def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) cherrypy.server.httpserver = self.httpserver_class # This is perhaps the wrong place for this call but this is the only # place that i've found so far that I KNOW is early enough to set this. cherrypy.config.update({'log.screen': False}) engine = cherrypy.engine if hasattr(engine, 'signal_handler'): engine.signal_handler.subscribe() if hasattr(engine, 'console_control_handler'): engine.console_control_handler.subscribe() def start(self, modulename=None): """Load and start the HTTP server.""" if modulename: # Unhook httpserver so cherrypy.server.start() creates a new # one (with config from setup_server, if declared). cherrypy.server.httpserver = None cherrypy.engine.start() self.sync_apps() def sync_apps(self): """Tell the server about any apps which the setup functions mounted.""" pass def stop(self): td = getattr(self, 'teardown', None) if td: td() cherrypy.engine.exit() for name, server in getattr(cherrypy, 'servers', {}).copy().items(): server.unsubscribe() del cherrypy.servers[name] class NativeServerSupervisor(LocalSupervisor): """Server supervisor for the builtin HTTP server.""" httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer' using_apache = False using_wsgi = False def __str__(self): return 'Builtin HTTP Server on %s:%s' % (self.host, self.port) class LocalWSGISupervisor(LocalSupervisor): """Server supervisor for the builtin WSGI server.""" httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer' using_apache = False using_wsgi = True def __str__(self): return 'Builtin WSGI Server on %s:%s' % (self.host, self.port) def sync_apps(self): """Hook a new WSGI app into the origin server.""" cherrypy.server.httpserver.wsgi_app = self.get_app() def get_app(self, app=None): """Obtain a new (decorated) WSGI app to hook into the origin server.""" if app is None: app = cherrypy.tree if self.validate: try: from wsgiref import validate except ImportError: warnings.warn( 'Error importing wsgiref. The validator will not run.') else: # wraps the app in the validator app = validate.validator(app) return app def get_cpmodpy_supervisor(**options): from cherrypy.test import modpy sup = modpy.ModPythonSupervisor(**options) sup.template = modpy.conf_cpmodpy return sup def get_modpygw_supervisor(**options): from cherrypy.test import modpy sup = modpy.ModPythonSupervisor(**options) sup.template = modpy.conf_modpython_gateway sup.using_wsgi = True return sup def get_modwsgi_supervisor(**options): from cherrypy.test import modwsgi return modwsgi.ModWSGISupervisor(**options) def get_modfcgid_supervisor(**options): from cherrypy.test import modfcgid return modfcgid.ModFCGISupervisor(**options) def get_modfastcgi_supervisor(**options): from cherrypy.test import modfastcgi return modfastcgi.ModFCGISupervisor(**options) def get_wsgi_u_supervisor(**options): cherrypy.server.wsgi_version = ('u', 0) return LocalWSGISupervisor(**options) class CPWebCase(webtest.WebCase): script_name = '' scheme = 'http' available_servers = {'wsgi': LocalWSGISupervisor, 'wsgi_u': get_wsgi_u_supervisor, 'native': NativeServerSupervisor, 'cpmodpy': get_cpmodpy_supervisor, 'modpygw': get_modpygw_supervisor, 'modwsgi': get_modwsgi_supervisor, 'modfcgid': get_modfcgid_supervisor, 'modfastcgi': get_modfastcgi_supervisor, } default_server = 'wsgi' @classmethod def _setup_server(cls, supervisor, conf): v = sys.version.split()[0] log.info('Python version used to run this test script: %s' % v) log.info('CherryPy version: %s' % cherrypy.__version__) if supervisor.scheme == 'https': ssl = ' (ssl)' else: ssl = '' log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl)) log.info('PID: %s' % os.getpid()) cherrypy.server.using_apache = supervisor.using_apache cherrypy.server.using_wsgi = supervisor.using_wsgi if sys.platform[:4] == 'java': cherrypy.config.update({'server.nodelay': False}) if isinstance(conf, text_or_bytes): parser = cherrypy.lib.reprconf.Parser() conf = parser.dict_from_file(conf).get('global', {}) else: conf = conf or {} baseconf = conf.copy() baseconf.update({'server.socket_host': supervisor.host, 'server.socket_port': supervisor.port, 'server.protocol_version': supervisor.protocol, 'environment': 'test_suite', }) if supervisor.scheme == 'https': # baseconf['server.ssl_module'] = 'builtin' baseconf['server.ssl_certificate'] = serverpem baseconf['server.ssl_private_key'] = serverpem # helper must be imported lazily so the coverage tool # can run against module-level statements within cherrypy. # Also, we have to do "from cherrypy.test import helper", # exactly like each test module does, because a relative import # would stick a second instance of webtest in sys.modules, # and we wouldn't be able to globally override the port anymore. if supervisor.scheme == 'https': webtest.WebCase.HTTP_CONN = HTTPSConnection return baseconf @classmethod def setup_class(cls): '' # Creates a server conf = { 'scheme': 'http', 'protocol': 'HTTP/1.1', 'port': 54583, 'host': '127.0.0.1', 'validate': False, 'server': 'wsgi', } supervisor_factory = cls.available_servers.get( conf.get('server', 'wsgi')) if supervisor_factory is None: raise RuntimeError('Unknown server in config: %s' % conf['server']) supervisor = supervisor_factory(**conf) # Copied from "run_test_suite" cherrypy.config.reset() baseconf = cls._setup_server(supervisor, conf) cherrypy.config.update(baseconf) setup_client() if hasattr(cls, 'setup_server'): # Clear the cherrypy tree and clear the wsgi server so that # it can be updated with the new root cherrypy.tree = cherrypy._cptree.Tree() cherrypy.server.httpserver = None cls.setup_server() # Add a resource for verifying there are no refleaks # to *every* test class. cherrypy.tree.mount(gctools.GCRoot(), '/gc') cls.do_gc_test = True supervisor.start(cls.__module__) cls.supervisor = supervisor @classmethod def teardown_class(cls): '' if hasattr(cls, 'setup_server'): cls.supervisor.stop() do_gc_test = False def test_gc(self): if not self.do_gc_test: return self.getPage('/gc/stats') try: self.assertBody('Statistics:') except Exception: 'Failures occur intermittently. See #1420' def prefix(self): return self.script_name.rstrip('/') def base(self): if ((self.scheme == 'http' and self.PORT == 80) or (self.scheme == 'https' and self.PORT == 443)): port = '' else: port = ':%s' % self.PORT return '%s://%s%s%s' % (self.scheme, self.HOST, port, self.script_name.rstrip('/')) def exit(self): sys.exit() def getPage(self, url, *args, **kwargs): """Open the url. """ if self.script_name: url = httputil.urljoin(self.script_name, url) return webtest.WebCase.getPage(self, url, *args, **kwargs) def skip(self, msg='skipped '): pytest.skip(msg) def assertErrorPage(self, status, message=None, pattern=''): """Compare the response body with a built in error page. The function will optionally look for the regexp pattern, within the exception embedded in the error page.""" # This will never contain a traceback page = cherrypy._cperror.get_error_page(status, message=message) # First, test the response body without checking the traceback. # Stick a match-all group (.*) in to grab the traceback. def esc(text): return re.escape(ntob(text)) epage = re.escape(page) epage = epage.replace( esc('
'),
            esc('
') + b'(.*)' + esc('
')) m = re.match(epage, self.body, re.DOTALL) if not m: self._handlewebError( 'Error page does not match; expected:\n' + page) return # Now test the pattern against the traceback if pattern is None: # Special-case None to mean that there should be *no* traceback. if m and m.group(1): self._handlewebError('Error page contains traceback') else: if (m is None) or ( not re.search(ntob(re.escape(pattern), self.encoding), m.group(1))): msg = 'Error page does not contain %s in traceback' self._handlewebError(msg % repr(pattern)) date_tolerance = 2 def assertEqualDates(self, dt1, dt2, seconds=None): """Assert abs(dt1 - dt2) is within Y seconds.""" if seconds is None: seconds = self.date_tolerance if dt1 > dt2: diff = dt1 - dt2 else: diff = dt2 - dt1 if not diff < datetime.timedelta(seconds=seconds): raise AssertionError('%r and %r are not within %r seconds.' % (dt1, dt2, seconds)) def _test_method_sorter(_, x, y): """Monkeypatch the test sorter to always run test_gc last in each suite.""" if x == 'test_gc': return 1 if y == 'test_gc': return -1 if x > y: return 1 if x < y: return -1 return 0 unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter def setup_client(): """Set up the WebCase classes to match the server's socket settings.""" webtest.WebCase.PORT = cherrypy.server.socket_port webtest.WebCase.HOST = cherrypy.server.socket_host if cherrypy.server.ssl_certificate: CPWebCase.scheme = 'https' # --------------------------- Spawning helpers --------------------------- # class CPProcess(object): pid_file = os.path.join(thisdir, 'test.pid') config_file = os.path.join(thisdir, 'test.conf') config_template = """[global] server.socket_host: '%(host)s' server.socket_port: %(port)s checker.on: False log.screen: False log.error_file: r'%(error_log)s' log.access_file: r'%(access_log)s' %(ssl)s %(extra)s """ error_log = os.path.join(thisdir, 'test.error.log') access_log = os.path.join(thisdir, 'test.access.log') def __init__(self, wait=False, daemonize=False, ssl=False, socket_host=None, socket_port=None): self.wait = wait self.daemonize = daemonize self.ssl = ssl self.host = socket_host or cherrypy.server.socket_host self.port = socket_port or cherrypy.server.socket_port def write_conf(self, extra=''): if self.ssl: serverpem = os.path.join(thisdir, 'test.pem') ssl = """ server.ssl_certificate: r'%s' server.ssl_private_key: r'%s' """ % (serverpem, serverpem) else: ssl = '' conf = self.config_template % { 'host': self.host, 'port': self.port, 'error_log': self.error_log, 'access_log': self.access_log, 'ssl': ssl, 'extra': extra, } with io.open(self.config_file, 'w', encoding='utf-8') as f: f.write(str(conf)) def start(self, imports=None): """Start cherryd in a subprocess.""" portend.free(self.host, self.port, timeout=1) args = [ '-m', 'cherrypy', '-c', self.config_file, '-p', self.pid_file, ] r""" Command for running cherryd server with autoreload enabled Using ``` ['-c', "__requires__ = 'CherryPy'; \ import pkg_resources, re, sys; \ sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \ sys.exit(\ pkg_resources.load_entry_point(\ 'CherryPy', 'console_scripts', 'cherryd')())"] ``` doesn't work as it's impossible to reconstruct the `-c`'s contents. Ref: https://github.com/cherrypy/cherrypy/issues/1545 """ if not isinstance(imports, (list, tuple)): imports = [imports] for i in imports: if i: args.append('-i') args.append(i) if self.daemonize: args.append('-d') env = os.environ.copy() # Make sure we import the cherrypy package in which this module is # defined. grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) if env.get('PYTHONPATH', ''): env['PYTHONPATH'] = os.pathsep.join( (grandparentdir, env['PYTHONPATH'])) else: env['PYTHONPATH'] = grandparentdir self._proc = subprocess.Popen([sys.executable] + args, env=env) if self.wait: self.exit_code = self._proc.wait() else: portend.occupied(self.host, self.port, timeout=5) # Give the engine a wee bit more time to finish STARTING if self.daemonize: time.sleep(2) else: time.sleep(1) def get_pid(self): if self.daemonize: with open(self.pid_file, 'rb') as f: return int(f.read()) return self._proc.pid def join(self): """Wait for the process to exit.""" if self.daemonize: return self._join_daemon() self._proc.wait() def _join_daemon(self): with contextlib.suppress(IOError): os.waitpid(self.get_pid(), 0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/logtest.py0000644252176402575230000002020514307416152021111 0ustar00jaracoprimarygroup"""logtest, a unittest.TestCase helper for testing log output.""" import sys import time from uuid import UUID import pytest from cherrypy._cpcompat import text_or_bytes try: # On Windows, msvcrt.getch reads a single char without output. import msvcrt def getchar(): return msvcrt.getch() except ImportError: # Unix getchr import tty import termios def getchar(): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class LogCase(object): """unittest.TestCase mixin for testing log messages. logfile: a filename for the desired log. Yes, I know modes are evil, but it makes the test functions so much cleaner to set this once. lastmarker: the last marker in the log. This can be used to search for messages since the last marker. markerPrefix: a string with which to prefix log markers. This should be unique enough from normal log output to use for marker identification. """ interactive = False logfile = None lastmarker = None markerPrefix = b'test suite marker: ' def _handleLogError(self, msg, data, marker, pattern): print('') print(' ERROR: %s' % msg) if not self.interactive: raise pytest.fail(msg) p = (' Show: ' '[L]og [M]arker [P]attern; ' '[I]gnore, [R]aise, or sys.e[X]it >> ') sys.stdout.write(p + ' ') # ARGH sys.stdout.flush() while True: i = getchar().upper() if i not in 'MPLIRX': continue print(i.upper()) # Also prints new line if i == 'L': for x, line in enumerate(data): if (x + 1) % self.console_height == 0: # The \r and comma should make the next line overwrite sys.stdout.write('<-- More -->\r ') m = getchar().lower() # Erase our "More" prompt sys.stdout.write(' \r ') if m == 'q': break print(line.rstrip()) elif i == 'M': print(repr(marker or self.lastmarker)) elif i == 'P': print(repr(pattern)) elif i == 'I': # return without raising the normal exception return elif i == 'R': raise pytest.fail(msg) elif i == 'X': self.exit() sys.stdout.write(p + ' ') def exit(self): sys.exit() def emptyLog(self): """Overwrite self.logfile with 0 bytes.""" with open(self.logfile, 'wb') as f: f.write('') def markLog(self, key=None): """Insert a marker line into the log and set self.lastmarker.""" if key is None: key = str(time.time()) self.lastmarker = key with open(self.logfile, 'ab+') as f: f.write( b'%s%s\n' % (self.markerPrefix, key.encode('utf-8')) ) def _read_marked_region(self, marker=None): """Return lines from self.logfile in the marked region. If marker is None, self.lastmarker is used. If the log hasn't been marked (using self.markLog), the entire log will be returned. """ # Give the logger time to finish writing? # time.sleep(0.5) logfile = self.logfile marker = marker or self.lastmarker if marker is None: with open(logfile, 'rb') as f: return f.readlines() if isinstance(marker, str): marker = marker.encode('utf-8') data = [] in_region = False with open(logfile, 'rb') as f: for line in f: if in_region: if (line.startswith(self.markerPrefix) and marker not in line): break else: data.append(line) elif marker in line: in_region = True return data def assertInLog(self, line, marker=None): """Fail if the given (partial) line is not in the log. The log will be searched from the given marker to the next marker. If marker is None, self.lastmarker is used. If the log hasn't been marked (using self.markLog), the entire log will be searched. """ data = self._read_marked_region(marker) for logline in data: if line in logline: return msg = '%r not found in log' % line self._handleLogError(msg, data, marker, line) def assertNotInLog(self, line, marker=None): """Fail if the given (partial) line is in the log. The log will be searched from the given marker to the next marker. If marker is None, self.lastmarker is used. If the log hasn't been marked (using self.markLog), the entire log will be searched. """ data = self._read_marked_region(marker) for logline in data: if line in logline: msg = '%r found in log' % line self._handleLogError(msg, data, marker, line) def assertValidUUIDv4(self, marker=None): """Fail if the given UUIDv4 is not valid. The log will be searched from the given marker to the next marker. If marker is None, self.lastmarker is used. If the log hasn't been marked (using self.markLog), the entire log will be searched. """ data = self._read_marked_region(marker) data = [ chunk.decode('utf-8').rstrip('\n').rstrip('\r') for chunk in data ] for log_chunk in data: try: uuid_log = data[-1] uuid_obj = UUID(uuid_log, version=4) except (TypeError, ValueError): pass # it might be in other chunk else: if str(uuid_obj) == uuid_log: return msg = '%r is not a valid UUIDv4' % uuid_log self._handleLogError(msg, data, marker, log_chunk) msg = 'UUIDv4 not found in log' self._handleLogError(msg, data, marker, log_chunk) def assertLog(self, sliceargs, lines, marker=None): """Fail if log.readlines()[sliceargs] is not contained in 'lines'. The log will be searched from the given marker to the next marker. If marker is None, self.lastmarker is used. If the log hasn't been marked (using self.markLog), the entire log will be searched. """ data = self._read_marked_region(marker) if isinstance(sliceargs, int): # Single arg. Use __getitem__ and allow lines to be str or list. if isinstance(lines, (tuple, list)): lines = lines[0] if isinstance(lines, str): lines = lines.encode('utf-8') if lines not in data[sliceargs]: msg = '%r not found on log line %r' % (lines, sliceargs) self._handleLogError( msg, [data[sliceargs], '--EXTRA CONTEXT--'] + data[ sliceargs + 1:sliceargs + 6], marker, lines) else: # Multiple args. Use __getslice__ and require lines to be list. if isinstance(lines, tuple): lines = list(lines) elif isinstance(lines, text_or_bytes): raise TypeError("The 'lines' arg must be a list when " "'sliceargs' is a tuple.") start, stop = sliceargs for line, logline in zip(lines, data[start:stop]): if isinstance(line, str): line = line.encode('utf-8') if line not in logline: msg = '%r not found in log' % line self._handleLogError(msg, data[start:stop], marker, line) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/modfastcgi.py0000644252176402575230000001077714307416152021565 0ustar00jaracoprimarygroup"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. To autostart fastcgi, the "apache" executable or script must be on your system path, or you must override the global APACHE_PATH. On some platforms, "apache" may be called "apachectl", "apache2ctl", or "httpd"--create a symlink to them if needed. You'll also need the WSGIServer from flup.servers. See http://projects.amor.org/misc/wiki/ModPythonGateway KNOWN BUGS ========== 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. 4. Apache replaces status "reason phrases" automatically. For example, CherryPy may set "304 Not modified" but Apache will write out "304 Not Modified" (capital "M"). 5. Apache does not allow custom error codes as per the spec. 6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the Request-URI too early. 7. mod_python will not read request bodies which use the "chunked" transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and mod_python's requestobject.c). 8. Apache will output a "Content-Length: 0" response header even if there's no response entity body. This isn't really a bug; it just differs from the CherryPy default. """ import os import re import cherrypy from cherrypy.process import servers from cherrypy.test import helper curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) def read_process(cmd, args=''): pipein, pipeout = os.popen4('%s %s' % (cmd, args)) try: firstline = pipeout.readline() if (re.search(r'(not recognized|No such file|not found)', firstline, re.IGNORECASE)): raise IOError('%s must be on your system path.' % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output APACHE_PATH = 'apache2ctl' CONF_PATH = 'fastcgi.conf' conf_fastcgi = """ # Apache2 server conf file for testing CherryPy with mod_fastcgi. # fumanchu: I had to hard-code paths due to crazy Debian layouts :( ServerRoot /usr/lib/apache2 User #1000 ErrorLog %(root)s/mod_fastcgi.error.log DocumentRoot "%(root)s" ServerName 127.0.0.1 Listen %(port)s LoadModule fastcgi_module modules/mod_fastcgi.so LoadModule rewrite_module modules/mod_rewrite.so Options +ExecCGI SetHandler fastcgi-script RewriteEngine On RewriteRule ^(.*)$ /fastcgi.pyc [L] FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 """ def erase_script_name(environ, start_response): environ['SCRIPT_NAME'] = '' return cherrypy.tree(environ, start_response) class ModFCGISupervisor(helper.LocalWSGISupervisor): httpserver_class = 'cherrypy.process.servers.FlupFCGIServer' using_apache = True using_wsgi = True template = conf_fastcgi def __str__(self): return 'FCGI Server on %s:%s' % (self.host, self.port) def start(self, modulename): cherrypy.server.httpserver = servers.FlupFCGIServer( application=erase_script_name, bindAddress=('127.0.0.1', 4000)) cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) cherrypy.server.socket_port = 4000 # For FCGI, we both start apache... self.start_apache() # ...and our local server cherrypy.engine.start() self.sync_apps() def start_apache(self): fcgiconf = CONF_PATH if not os.path.isabs(fcgiconf): fcgiconf = os.path.join(curdir, fcgiconf) # Write the Apache conf file. with open(fcgiconf, 'wb') as f: server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] output = self.template % {'port': self.port, 'root': curdir, 'server': server} output = output.replace('\r\n', '\n') f.write(output) result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) if result: print(result) def stop(self): """Gracefully shutdown a server that is serving forever.""" read_process(APACHE_PATH, '-k stop') helper.LocalWSGISupervisor.stop(self) def sync_apps(self): cherrypy.server.httpserver.fcgiserver.application = self.get_app( erase_script_name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/modfcgid.py0000644252176402575230000001014014307416152021201 0ustar00jaracoprimarygroup"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. To autostart fcgid, the "apache" executable or script must be on your system path, or you must override the global APACHE_PATH. On some platforms, "apache" may be called "apachectl", "apache2ctl", or "httpd"--create a symlink to them if needed. You'll also need the WSGIServer from flup.servers. See http://projects.amor.org/misc/wiki/ModPythonGateway KNOWN BUGS ========== 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. 4. Apache replaces status "reason phrases" automatically. For example, CherryPy may set "304 Not modified" but Apache will write out "304 Not Modified" (capital "M"). 5. Apache does not allow custom error codes as per the spec. 6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the Request-URI too early. 7. mod_python will not read request bodies which use the "chunked" transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and mod_python's requestobject.c). 8. Apache will output a "Content-Length: 0" response header even if there's no response entity body. This isn't really a bug; it just differs from the CherryPy default. """ import os import re import cherrypy from cherrypy._cpcompat import ntob from cherrypy.process import servers from cherrypy.test import helper curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) def read_process(cmd, args=''): pipein, pipeout = os.popen4('%s %s' % (cmd, args)) try: firstline = pipeout.readline() if (re.search(r'(not recognized|No such file|not found)', firstline, re.IGNORECASE)): raise IOError('%s must be on your system path.' % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output APACHE_PATH = 'httpd' CONF_PATH = 'fcgi.conf' conf_fcgid = """ # Apache2 server conf file for testing CherryPy with mod_fcgid. DocumentRoot "%(root)s" ServerName 127.0.0.1 Listen %(port)s LoadModule fastcgi_module modules/mod_fastcgi.dll LoadModule rewrite_module modules/mod_rewrite.so Options ExecCGI SetHandler fastcgi-script RewriteEngine On RewriteRule ^(.*)$ /fastcgi.pyc [L] FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 """ class ModFCGISupervisor(helper.LocalSupervisor): using_apache = True using_wsgi = True template = conf_fcgid def __str__(self): return 'FCGI Server on %s:%s' % (self.host, self.port) def start(self, modulename): cherrypy.server.httpserver = servers.FlupFCGIServer( application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) # For FCGI, we both start apache... self.start_apache() # ...and our local server helper.LocalServer.start(self, modulename) def start_apache(self): fcgiconf = CONF_PATH if not os.path.isabs(fcgiconf): fcgiconf = os.path.join(curdir, fcgiconf) # Write the Apache conf file. with open(fcgiconf, 'wb') as f: server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] output = self.template % {'port': self.port, 'root': curdir, 'server': server} output = ntob(output.replace('\r\n', '\n')) f.write(output) result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) if result: print(result) def stop(self): """Gracefully shutdown a server that is serving forever.""" read_process(APACHE_PATH, '-k stop') helper.LocalServer.stop(self) def sync_apps(self): cherrypy.server.httpserver.fcgiserver.application = self.get_app() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/modpy.py0000644252176402575230000001150514307416152020563 0ustar00jaracoprimarygroup"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. To autostart modpython, the "apache" executable or script must be on your system path, or you must override the global APACHE_PATH. On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- create a symlink to them if needed. If you wish to test the WSGI interface instead of our _cpmodpy interface, you also need the 'modpython_gateway' module at: http://projects.amor.org/misc/wiki/ModPythonGateway KNOWN BUGS ========== 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. 4. Apache replaces status "reason phrases" automatically. For example, CherryPy may set "304 Not modified" but Apache will write out "304 Not Modified" (capital "M"). 5. Apache does not allow custom error codes as per the spec. 6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the Request-URI too early. 7. mod_python will not read request bodies which use the "chunked" transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and mod_python's requestobject.c). 8. Apache will output a "Content-Length: 0" response header even if there's no response entity body. This isn't really a bug; it just differs from the CherryPy default. """ import os import re import cherrypy from cherrypy.test import helper curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) def read_process(cmd, args=''): pipein, pipeout = os.popen4('%s %s' % (cmd, args)) try: firstline = pipeout.readline() if (re.search(r'(not recognized|No such file|not found)', firstline, re.IGNORECASE)): raise IOError('%s must be on your system path.' % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output APACHE_PATH = 'httpd' CONF_PATH = 'test_mp.conf' conf_modpython_gateway = """ # Apache2 server conf file for testing CherryPy with modpython_gateway. ServerName 127.0.0.1 DocumentRoot "/" Listen %(port)s LoadModule python_module modules/mod_python.so SetHandler python-program PythonFixupHandler cherrypy.test.modpy::wsgisetup PythonOption testmod %(modulename)s PythonHandler modpython_gateway::handler PythonOption wsgi.application cherrypy::tree PythonOption socket_host %(host)s PythonDebug On """ conf_cpmodpy = """ # Apache2 server conf file for testing CherryPy with _cpmodpy. ServerName 127.0.0.1 DocumentRoot "/" Listen %(port)s LoadModule python_module modules/mod_python.so SetHandler python-program PythonFixupHandler cherrypy.test.modpy::cpmodpysetup PythonHandler cherrypy._cpmodpy::handler PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server PythonOption socket_host %(host)s PythonDebug On """ class ModPythonSupervisor(helper.Supervisor): using_apache = True using_wsgi = False template = None def __str__(self): return 'ModPython Server on %s:%s' % (self.host, self.port) def start(self, modulename): mpconf = CONF_PATH if not os.path.isabs(mpconf): mpconf = os.path.join(curdir, mpconf) with open(mpconf, 'wb') as f: f.write(self.template % {'port': self.port, 'modulename': modulename, 'host': self.host}) result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) if result: print(result) def stop(self): """Gracefully shutdown a server that is serving forever.""" read_process(APACHE_PATH, '-k stop') loaded = False def wsgisetup(req): global loaded if not loaded: loaded = True options = req.get_options() cherrypy.config.update({ 'log.error_file': os.path.join(curdir, 'test.log'), 'environment': 'test_suite', 'server.socket_host': options['socket_host'], }) modname = options['testmod'] mod = __import__(modname, globals(), locals(), ['']) mod.setup_server() cherrypy.server.unsubscribe() cherrypy.engine.start() from mod_python import apache return apache.OK def cpmodpysetup(req): global loaded if not loaded: loaded = True options = req.get_options() cherrypy.config.update({ 'log.error_file': os.path.join(curdir, 'test.log'), 'environment': 'test_suite', 'server.socket_host': options['socket_host'], }) from mod_python import apache return apache.OK ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/modwsgi.py0000644252176402575230000001126514307416152021107 0ustar00jaracoprimarygroup"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. To autostart modwsgi, the "apache" executable or script must be on your system path, or you must override the global APACHE_PATH. On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- create a symlink to them if needed. KNOWN BUGS ========== 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. 4. Apache replaces status "reason phrases" automatically. For example, CherryPy may set "304 Not modified" but Apache will write out "304 Not Modified" (capital "M"). 5. Apache does not allow custom error codes as per the spec. 6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the Request-URI too early. 7. mod_wsgi will not read request bodies which use the "chunked" transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and mod_python's requestobject.c). 8. When responding with 204 No Content, mod_wsgi adds a Content-Length header for you. 9. When an error is raised, mod_wsgi has no facility for printing a traceback as the response content (it's sent to the Apache log instead). 10. Startup and shutdown of Apache when running mod_wsgi seems slow. """ import os import re import sys import time import portend from cheroot.test import webtest import cherrypy from cherrypy.test import helper curdir = os.path.abspath(os.path.dirname(__file__)) def read_process(cmd, args=''): pipein, pipeout = os.popen4('%s %s' % (cmd, args)) try: firstline = pipeout.readline() if (re.search(r'(not recognized|No such file|not found)', firstline, re.IGNORECASE)): raise IOError('%s must be on your system path.' % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output if sys.platform == 'win32': APACHE_PATH = 'httpd' else: APACHE_PATH = 'apache' CONF_PATH = 'test_mw.conf' conf_modwsgi = r""" # Apache2 server conf file for testing CherryPy with modpython_gateway. ServerName 127.0.0.1 DocumentRoot "/" Listen %(port)s AllowEncodedSlashes On LoadModule rewrite_module modules/mod_rewrite.so RewriteEngine on RewriteMap escaping int:escape LoadModule log_config_module modules/mod_log_config.so LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined CustomLog "%(curdir)s/apache.access.log" combined ErrorLog "%(curdir)s/apache.error.log" LogLevel debug LoadModule wsgi_module modules/mod_wsgi.so LoadModule env_module modules/mod_env.so WSGIScriptAlias / "%(curdir)s/modwsgi.py" SetEnv testmod %(testmod)s """ # noqa E501 class ModWSGISupervisor(helper.Supervisor): """Server Controller for ModWSGI and CherryPy.""" using_apache = True using_wsgi = True template = conf_modwsgi def __str__(self): return 'ModWSGI Server on %s:%s' % (self.host, self.port) def start(self, modulename): mpconf = CONF_PATH if not os.path.isabs(mpconf): mpconf = os.path.join(curdir, mpconf) with open(mpconf, 'wb') as f: output = (self.template % {'port': self.port, 'testmod': modulename, 'curdir': curdir}) f.write(output) result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) if result: print(result) # Make a request so mod_wsgi starts up our app. # If we don't, concurrent initial requests will 404. portend.occupied('127.0.0.1', self.port, timeout=5) webtest.openURL('/ihopetheresnodefault', port=self.port) time.sleep(1) def stop(self): """Gracefully shutdown a server that is serving forever.""" read_process(APACHE_PATH, '-k stop') loaded = False def application(environ, start_response): global loaded if not loaded: loaded = True modname = 'cherrypy.test.' + environ['testmod'] mod = __import__(modname, globals(), locals(), ['']) mod.setup_server() cherrypy.config.update({ 'log.error_file': os.path.join(curdir, 'test.error.log'), 'log.access_file': os.path.join(curdir, 'test.access.log'), 'environment': 'test_suite', 'engine.SIGHUP': None, 'engine.SIGTERM': None, }) return cherrypy.tree(environ, start_response) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/sessiondemo.py0000755252176402575230000001246114307416152021770 0ustar00jaracoprimarygroup#!/usr/bin/python """A session demonstration app.""" import calendar from datetime import datetime import sys import cherrypy from cherrypy.lib import sessions page = """

Session Demo

Reload this page. The session ID should not change from one reload to the next

Index | Expire | Regenerate

Session ID:%(sessionid)s

%(changemsg)s

Request Cookie%(reqcookie)s
Response Cookie%(respcookie)s

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
""" # noqa E501 class Root(object): def page(self): changemsg = [] if cherrypy.session.id != cherrypy.session.originalid: if cherrypy.session.originalid is None: changemsg.append( 'Created new session because no session id was given.') if cherrypy.session.missing: changemsg.append( 'Created new session due to missing ' '(expired or malicious) session.') if cherrypy.session.regenerated: changemsg.append('Application generated a new session.') try: expires = cherrypy.response.cookie['session_id']['expires'] except KeyError: expires = '' return page % { 'sessionid': cherrypy.session.id, 'changemsg': '
'.join(changemsg), 'respcookie': cherrypy.response.cookie.output(), 'reqcookie': cherrypy.request.cookie.output(), 'sessiondata': list(cherrypy.session.items()), 'servertime': ( datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' ), 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), 'cpversion': cherrypy.__version__, 'pyversion': sys.version, 'expires': expires, } @cherrypy.expose def index(self): # Must modify data or the session will not be saved. cherrypy.session['color'] = 'green' return self.page() @cherrypy.expose def expire(self): sessions.expire() return self.page() @cherrypy.expose def regen(self): cherrypy.session.regenerate() # Must modify data or the session will not be saved. cherrypy.session['color'] = 'yellow' return self.page() if __name__ == '__main__': cherrypy.config.update({ # 'environment': 'production', 'log.screen': True, 'tools.sessions.on': True, }) cherrypy.quickstart(Root()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5423675 CherryPy-18.9.0/cherrypy/test/static/0000755252176402575230000000000014536340235020350 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/static/404.html0000644252176402575230000000013414307416152021541 0ustar00jaracoprimarygroup

I couldn't find that thing you were looking for!

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/static/dirback.jpg0000644252176402575230000004031114307416152022446 0ustar00jaracoprimarygroupJFIFC d!1"A2BQRabqr#3CcS ?Ƿ/Pӽ+@(0 Y1 pPo tR"`mE(Ma~H>M+R &T Ƀ$(VߗHWo ّ~ϣ1o(ʰL812s?`Z,toc2 _;UBst1MQ/qb@01T/H #JQވ?Un=0$겑ϵ5QdbDбcdj0wR9p W[Azs{Mbi$!mRZr\`-fZr"y ͏U"lHr4ROI憧b$R>"P;U <"UK7TËr%`{P/ٙܩ wfE PJwMFC6T!;ܐ`jk _@VVd 'ia a> LRIR!طE֌k'bsK}RCn36k:nA[3To_ фC{`h#0ޤMFu@: ;jPw=P`"s9֥PrċJ401X$^a웦Ekq_iXشh?J7J KSClrt " gM1FBH4>T3@+A†`hGh5U Wh.RE [n^%  a[`Aq P ;oT:ϒ ׹A: { ٻ ZH?B(TGn' X66Ʉ'EI?^59آ\ oJxQME5#RH )R[#i&"A>dTN+&<f#9TztgH#98 7RTkqq *i1z*Q7[hr 0 J@ο[C2$?SI&MzHӀP[\1D7r @D@4' 0kRiقE&49Ӛ`d\iAR ;-)P ˄iEΆDrUGMu $ra:[͢{  F@h0w>;L;@WYRJMךsͼJpR!p$h`u@cZF9Z+./_4dka8Umb5d1`)fS*o!>hJ"j#"[ASGuϥBoo,+qSç9NF9d1wTu5L A*kO0r{]OQ0dR xj;kDsČ0pz`E?TckR "r4Y(};RFJ@әF7 @=E)Qf1%4>хW#8Hl 8Dv u*vA*hT_+B`G @2M G*-0ݷ}ˎۼja͸mʤ0oֿӫvȥGu]i"&YT9EjB49]<+f\iRHA1ĕ5Nu%i5{ =Tת63nP;mZn(ˢh?"4ykT^Ķ<:W>R>+YFAcIsyڬ x_@Lok|+t3OCwD@CCSFLݵщfo*ViDI+y4(,bE"0Eѧⵑ _0@*N`,sw MlyZ z Cf*0(A>>9n JjHI46xrU'|͢  Gwx94W5+B4w4 D%DڷKoSf*J!#i[`=ZqnwppdaI-$ َW1Vٱt3U mmiՁv` H* wdH/_YA5PnQF ,/Jsq,j \6fS ^H(ƚN U ӽZȓ B3:dj'GZ8q : )Zx {APx=h#kݾ4,_jOdtܔ 'B:$ N倴;@ :hy&湂1c\TM`S1F\hW@h",D`!+Rj0΅;9I榺7E- ! 8jAt תpi(T9ҀӹvAoRQs Sb.=Itgl0S.ᴪ A@`؛ߒz A)l3AXAOǻʼnPf&+c|;] nJˤ1S)R(k AUvT 4mX6vRLXᲦV@U_4=V',5Ϊ)J6|Tu ?ίLѾ%B&7P8 C@5NisvefZ+(1%W]kD<֌A*+hCo:0@~-Sf)TdF(PȤ*NT4ADk&s`옼SftT 9MKo_df  7D/4ӚQTɫRO  ¥&&@. l|$!: MCC`SM$fD@n^0 2P4u5n'N~lۥۼKFkZ {H(`IZ4#̪ @#Z1+vXQh_PߋyR쁽AAI [t՟Zdo}W#Z>G9{Ha00{HiPV+Ƀ;҉zHĈVl#H%p䁁 4 쌈_MT( G?Gh;:O;QkY+)-&h> 7A̠@J;Z}jsoNXt''W1@H=ШbtX8\@(mdĂ m*O+K{ 'nk;)L ,CI hgZ(/(6l ٰkȪ֥7젰ʄCY[wpJ5/7Z+ #$sC Z6(i2A5TmOz _*Z|c[uߥ5*wn}khf; 7ԩ@C{ 1};M;:T[S-&ӦWTlU#v{ЊaP>/Bf>! PLԁ4q_´WdoR L- J 1 NH ]JȐ?R4u;M-g, ꀜT7/ .잨$hP8$5R4u9ǭm2E,t%^ѷ% i jK@#1T'g F)R1jD7X֗@ktZf)G5A$b0lz!_[=Pl `#144=V`(rwgH9Y8~4ѰҪq8Fk`:@s[2&~ڃ}J=PXNh 7yTI1f]{֕wf8Q'q}\k`p;@H@}Poc(xiHTܼOAհ9MIs-;ԂPQR@cΨ)RjD`h S{jrNV!GHĘ6T8;A3(nZP?`PD4@cMU`7wJpߟ4 HlīI@;ְ͘{Sh>(7תVD\*i?BսZ6m(-}h/Z8T@[ v77ÅPZDK߉RO^ ]P7R$R+5'Td;$SI@9KQj 5 z޳ωH3B-h- _!(iX14kO pIh#lD@~uSF@ۧ{R&NkZfon43@ `؏@KDhls^+J b!dQKjmn.H .x1Y9fDLG@dZGA$∉+ :9jD>3M Z(֌/c~ׇ։_a2d6\O@$=,{PpZ/J4CG"Nq1_gS>,|JT0 &yR apg75\Vֱ@L@+C{| yAAF$bMB@h1ew_Z V = nq6ѳ7c `v*E 5V gXI0sR&Z3PGƿif(HpZ w od_Z oְ>9Nzd6SoU{Q攈&9kE(P64NR+4z @mpPHSF|)QZD{¡-cghDCm1q 6귕tI.i(@hݝpQKEAɈv@fi~3Lfuͥ îAF4( |UA4g v)J@_t~,?pR7 쬿kـh+ȪU^IZGDЃSF#NKyQs~jTR((,];zM+'Wm' 1ANS@:_}( _HR+ic6tutX9Z]C|P0o¡`H$ s@A-Iq 1VԲ~TymZ)57@"@ *XS17tVrrת+ )5%`@B` Z@I G#塀yUP3Q#~D3 AyvMj/:*JDw;"D^ "҉@ce/rhTJX#1SNH_ZHf6T՚CqPrȂ3AdX*b")T%Q" sZi@M5vU@PC)Ħ 6m}h-%Q.wסJIC7,HuІڳ(StT CQbS6Nvjdd:9gT&H*j,HWZO U8ÍGX#]7ؠh&hiyE3ptvqƍ)Fd:-X4h@zߪDH4Slk)h PR.?mh $д4 9E!*?Iz ԙumjPFƃ1ּJT 1!HehjɋD+ ʀ8$ P:lULݪ l\]3-VUp;  on?"ڧWzn"jV_W*@CY"@x 7сȡZ&{HւR)VЂUЀKl::T;@nz\pE"ZzFTɘX3P +rJXtRF8<\&w@3iSAȪ 2& 3d_@'(|G#MF9ͺ/)Qf3 ; 텵%(rRs|=$ &~P&ayl LZX ډ#~߹`_ҁ<;?~Ku33رJ47¨0'z.:4e5+¨Kkh8nrJ<'wyԨPDqx[b\)P<_x4Ů2@d]h3Ϛ)z#C-@A3:Jt$]4RW$Jn9[~$T[X,@zTTHuJc5kĤ RlìU >ktz[P.h 젴wKC{ `U:[*(&ۼZzk8$ eƵR7$eZkA\N UT4t(+D y`0f"ZG|$1_D ΊbyX3:du@@5ZG;r} U 8Œ-$)SkyFȭ lAL {(SLZ@hF胟j3qٿhSWn bM6A?DZH;0uȡG,Uh<8;֊}wd]-t $0U r"R66KԨACF7<ґ5V9VfREC7) }*| zMrwN/F;. X98PA:*EMZ/ڲ)]:K04*`3Ui@R)+(>]T{@*z-KOA Ǻ+lGOb)KC"s7eJrTgЌ\j?kf=$cjHeuʃݺT'Ĕt!t3\5;# `扦wIRT@AV{ֱr2xJH263ƵmQ)qٻu1}m8xhN_Ƥ9@ |H:v_ڨA1lUNAV1e} PPnA7UWtߟ2#Bƛb7ʼneLs0 #f&ըE0]`J RdYPp" ďq""&POj(K1(AAF&`T+f&AY FPD9ӥ=ݳĴ3FwH6iA2S"]ɽn  PTʐ`RP wpdR'N- GuP `@>g t"E7Z;AТJ9u[G," ԥ&m No ā9؀ 4wZFд0%sbW' 9E: ]L숆x6w}8!(P.Ѧh-_ХLZG  ]RQ *bE KLv-gLhhM#,ĨFײ[XZ W*{CuZ'^R ɭkNSGU'(\4ct@d,8Nw^C-s ks:õ<@*;H,90Vȕ`4ܪ%q]Ʀ\@*;7v/B%}%HqϦmBZo]( F[QMq#,NfT1w;<_~*g4Cr]Lu$0Ȋ-.Z'*) -T4RAl57ugֹуMr~Q.sĉUpn(-.[SϒP,U6ʁrX>ғ]J Ϳkƒxk kR@F.vj idPĠPsfkQ2TdWv#YXkDqH Ulu@dǟTe"L X̊Z m>g JTyM#&\zPcN']HN_D`~i -PlC\,pJ$}[A$ 8Qwf߱*bH]v n(X #88AAvCIҫqڱS@@A=P#np¦G1wG*1,0-ĪӰU+υh`LK* OAaa]SS\V)Q79M"jT9H0HZGʡ@#d }U`>P5fߟ!x`8 4Inq|)@U@@̠[G0M`Eõff*2@Q1AV5 Z,hbf4FAAUwo SU ƍ2Ɔ!:PqkWIHACLJ:I[L nPhk ^֪B>Ly,A_vDsG~B /4[ povx|H5n v;?F:@vyB r@CWe&Xۛq* ѳY9 տ"vn -8wX8wUiq -״1H~h$ZHlŦC*pG@eq # U-ʧ`?|-UG l@E^kG4sA Ƥ.7W'fRٸhjL3VVa-& d [*:AWM``#P=}H'Se"@2Ĵ[cot-I` {D=idE ۦHP;±ٌ‡2{b`x[t.TA<Ԛh zΌFBP 5dh$Z@i %ѫEJ "KPĻ^Ry@zX9N&Ҏ'4Gߟc~vm^"-p5$LGf^;_lFpTV6AI=Q#_[B`V*D03M~:vNY%E*}?lSWfH B V0P.ڷULH=g? SrJ3FW4s9vZ\kHaVZy48_H@N]Wi|ʹ9nA egHIh| 4ִP66?Դo28ȯC2m?HP8=6#CDO#nA+ @Am< m(?o iµ!0DZku` k^n=P?^)or} U ۯKJјn_ 퀖uª-j `pl&R`gtuR 쎿f$BfA1?{)P1'2lOud揾ʡP65`\o&Tvo{>;V-v%QJAjw9@"oH؟A0@ߧ7Ƞ3&w0%*r kdxP8R@9{ R'B{iAw@X:MNzѻusT٪nҩN$MȢg4EpmP3e1 :pIgG 9uZ@cN1cpo~@n5C8nTIٹXt@bu*xRm&IPU&ENHFkإ]䩎}8a u5QPٳu=5#P'm ,IwY # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 from hashlib import md5 import cherrypy from cherrypy._cpcompat import ntob from cherrypy.lib import auth_basic from cherrypy.test import helper class BasicAuthTest(helper.CPWebCase): @staticmethod def setup_server(): class Root: @cherrypy.expose def index(self): return 'This is public.' class BasicProtected: @cherrypy.expose def index(self): return "Hello %s, you've been authorized." % ( cherrypy.request.login) class BasicProtected2: @cherrypy.expose def index(self): return "Hello %s, you've been authorized." % ( cherrypy.request.login) class BasicProtected2_u: @cherrypy.expose def index(self): return "Hello %s, you've been authorized." % ( cherrypy.request.login) userpassdict = {'xuser': 'xpassword'} userhashdict = {'xuser': md5(b'xpassword').hexdigest()} userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()} def checkpasshash(realm, user, password): p = userhashdict.get(user) return p and p == md5(ntob(password)).hexdigest() or False def checkpasshash_u(realm, user, password): p = userhashdict_u.get(user) return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict) conf = { '/basic': { 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'wonderland', 'tools.auth_basic.checkpassword': basic_checkpassword_dict }, '/basic2': { 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'wonderland', 'tools.auth_basic.checkpassword': checkpasshash, 'tools.auth_basic.accept_charset': 'ISO-8859-1', }, '/basic2_u': { 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'wonderland', 'tools.auth_basic.checkpassword': checkpasshash_u, 'tools.auth_basic.accept_charset': 'UTF-8', }, } root = Root() root.basic = BasicProtected() root.basic2 = BasicProtected2() root.basic2_u = BasicProtected2_u() cherrypy.tree.mount(root, config=conf) def testPublic(self): self.getPage('/') self.assertStatus('200 OK') self.assertHeader('Content-Type', 'text/html;charset=utf-8') self.assertBody('This is public.') def testBasic(self): self.getPage('/basic/') self.assertStatus(401) self.assertHeader( 'WWW-Authenticate', 'Basic realm="wonderland", charset="UTF-8"' ) self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) self.assertStatus(401) self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) self.assertStatus('200 OK') self.assertBody("Hello xuser, you've been authorized.") def testBasic2(self): self.getPage('/basic2/') self.assertStatus(401) self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) self.assertStatus(401) self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) self.assertStatus('200 OK') self.assertBody("Hello xuser, you've been authorized.") def testBasic2_u(self): self.getPage('/basic2_u/') self.assertStatus(401) self.assertHeader( 'WWW-Authenticate', 'Basic realm="wonderland", charset="UTF-8"' ) self.getPage('/basic2_u/', [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) self.assertStatus(401) self.getPage('/basic2_u/', [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) self.assertStatus('200 OK') self.assertBody("Hello xюзер, you've been authorized.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_auth_digest.py0000644252176402575230000001054614307416152022776 0ustar00jaracoprimarygroup# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 import cherrypy from cherrypy.lib import auth_digest from cherrypy._cpcompat import ntob from cherrypy.test import helper def _fetch_users(): return {'test': 'test', '☃йюзер': 'їпароль'} get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users()) class DigestAuthTest(helper.CPWebCase): @staticmethod def setup_server(): class Root: @cherrypy.expose def index(self): return 'This is public.' class DigestProtected: @cherrypy.expose def index(self, *args, **kwargs): return "Hello %s, you've been authorized." % ( cherrypy.request.login) conf = {'/digest': {'tools.auth_digest.on': True, 'tools.auth_digest.realm': 'localhost', 'tools.auth_digest.get_ha1': get_ha1, 'tools.auth_digest.key': 'a565c27146791cfb', 'tools.auth_digest.debug': True, 'tools.auth_digest.accept_charset': 'UTF-8'}} root = Root() root.digest = DigestProtected() cherrypy.tree.mount(root, config=conf) def testPublic(self): self.getPage('/') assert self.status == '200 OK' self.assertHeader('Content-Type', 'text/html;charset=utf-8') assert self.body == b'This is public.' def _test_parametric_digest(self, username, realm): test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path' self.getPage(test_uri) assert self.status_code == 401 msg = 'Digest authentification scheme was not found' www_auth_digest = tuple(filter( lambda kv: kv[0].lower() == 'www-authenticate' and kv[1].startswith('Digest '), self.headers, )) assert len(www_auth_digest) == 1, msg items = www_auth_digest[0][-1][7:].split(', ') tokens = {} for item in items: key, value = item.split('=') tokens[key.lower()] = value assert tokens['realm'] == '"localhost"' assert tokens['algorithm'] == '"MD5"' assert tokens['qop'] == '"auth"' assert tokens['charset'] == '"UTF-8"' nonce = tokens['nonce'].strip('"') # Test user agent response with a wrong value for 'realm' base_auth = ('Digest username="%s", ' 'realm="%s", ' 'nonce="%s", ' 'uri="%s", ' 'algorithm=MD5, ' 'response="%s", ' 'qop=auth, ' 'nc=%s, ' 'cnonce="1522e61005789929"') encoded_user = username encoded_user = encoded_user.encode('utf-8') encoded_user = encoded_user.decode('latin1') auth_header = base_auth % ( encoded_user, realm, nonce, test_uri, '11111111111111111111111111111111', '00000001', ) auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') # calculate the response digest ha1 = get_ha1(auth.realm, auth.username) response = auth.request_digest(ha1) auth_header = base_auth % ( encoded_user, realm, nonce, test_uri, response, '00000001', ) self.getPage(test_uri, [('Authorization', auth_header)]) def test_wrong_realm(self): # send response with correct response digest, but wrong realm self._test_parametric_digest(username='test', realm='wrong realm') assert self.status_code == 401 def test_ascii_user(self): self._test_parametric_digest(username='test', realm='localhost') assert self.status == '200 OK' assert self.body == b"Hello test, you've been authorized." def test_unicode_user(self): self._test_parametric_digest(username='☃йюзер', realm='localhost') assert self.status == '200 OK' assert self.body == ntob( "Hello ☃йюзер, you've been authorized.", 'utf-8', ) def test_wrong_scheme(self): basic_auth = { 'Authorization': 'Basic foo:bar', } self.getPage('/digest/', headers=list(basic_auth.items())) assert self.status_code == 401 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_bus.py0000644252176402575230000002335014307416152021264 0ustar00jaracoprimarygroup"""Publish-subscribe bus tests.""" # pylint: disable=redefined-outer-name import os import sys import threading import time import unittest.mock import pytest from cherrypy.process import wspbus CI_ON_MACOS = bool(os.getenv('CI')) and sys.platform == 'darwin' msg = 'Listener %d on channel %s: %s.' # pylint: disable=invalid-name @pytest.fixture def bus(): """Return a wspbus instance.""" return wspbus.Bus() @pytest.fixture def log_tracker(bus): """Return an instance of bus log tracker.""" class LogTracker: # pylint: disable=too-few-public-methods """Bus log tracker.""" log_entries = [] def __init__(self, bus): def logit(msg, level): # pylint: disable=unused-argument self.log_entries.append(msg) bus.subscribe('log', logit) return LogTracker(bus) @pytest.fixture def listener(): """Return an instance of bus response tracker.""" class Listner: # pylint: disable=too-few-public-methods """Bus handler return value tracker.""" responses = [] def get_listener(self, channel, index): """Return an argument tracking listener.""" def listener(arg=None): self.responses.append(msg % (index, channel, arg)) return listener return Listner() def test_builtin_channels(bus, listener): """Test that built-in channels trigger corresponding listeners.""" expected = [] for channel in bus.listeners: for index, priority in enumerate([100, 50, 0, 51]): bus.subscribe( channel, listener.get_listener(channel, index), priority, ) for channel in bus.listeners: bus.publish(channel) expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) bus.publish(channel, arg=79347) expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) assert listener.responses == expected def test_custom_channels(bus, listener): """Test that custom pub-sub channels work as built-in ones.""" expected = [] custom_listeners = ('hugh', 'louis', 'dewey') for channel in custom_listeners: for index, priority in enumerate([None, 10, 60, 40]): bus.subscribe( channel, listener.get_listener(channel, index), priority, ) for channel in custom_listeners: bus.publish(channel, 'ah so') expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)) bus.publish(channel) expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2)) assert listener.responses == expected def test_listener_errors(bus, listener): """Test that unhandled exceptions raise channel failures.""" expected = [] channels = [c for c in bus.listeners if c != 'log'] for channel in channels: bus.subscribe(channel, listener.get_listener(channel, 1)) # This will break since the lambda takes no args. bus.subscribe(channel, lambda: None, priority=20) for channel in channels: with pytest.raises(wspbus.ChannelFailures): bus.publish(channel, 123) expected.append(msg % (1, channel, 123)) assert listener.responses == expected def test_start(bus, listener, log_tracker): """Test that bus start sequence calls all listeners.""" num = 3 for index in range(num): bus.subscribe('start', listener.get_listener('start', index)) bus.start() try: # The start method MUST call all 'start' listeners. assert ( set(listener.responses) == set(msg % (i, 'start', None) for i in range(num))) # The start method MUST move the state to STARTED # (or EXITING, if errors occur) assert bus.state == bus.states.STARTED # The start method MUST log its states. assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED'] finally: # Exit so the atexit handler doesn't complain. bus.exit() def test_stop(bus, listener, log_tracker): """Test that bus stop sequence calls all listeners.""" num = 3 for index in range(num): bus.subscribe('stop', listener.get_listener('stop', index)) bus.stop() # The stop method MUST call all 'stop' listeners. assert (set(listener.responses) == set(msg % (i, 'stop', None) for i in range(num))) # The stop method MUST move the state to STOPPED assert bus.state == bus.states.STOPPED # The stop method MUST log its states. assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED'] def test_graceful(bus, listener, log_tracker): """Test that bus graceful state triggers all listeners.""" num = 3 for index in range(num): bus.subscribe('graceful', listener.get_listener('graceful', index)) bus.graceful() # The graceful method MUST call all 'graceful' listeners. assert ( set(listener.responses) == set(msg % (i, 'graceful', None) for i in range(num))) # The graceful method MUST log its states. assert log_tracker.log_entries == ['Bus graceful'] def test_exit(bus, listener, log_tracker): """Test that bus exit sequence is correct.""" num = 3 for index in range(num): bus.subscribe('stop', listener.get_listener('stop', index)) bus.subscribe('exit', listener.get_listener('exit', index)) bus.exit() # The exit method MUST call all 'stop' listeners, # and then all 'exit' listeners. assert (set(listener.responses) == set([msg % (i, 'stop', None) for i in range(num)] + [msg % (i, 'exit', None) for i in range(num)])) # The exit method MUST move the state to EXITING assert bus.state == bus.states.EXITING # The exit method MUST log its states. assert (log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) def test_wait(bus): """Test that bus wait awaits for states.""" def f(method): # pylint: disable=invalid-name time.sleep(0.2) getattr(bus, method)() flow = [ ('start', [bus.states.STARTED]), ('stop', [bus.states.STOPPED]), ('start', [bus.states.STARTING, bus.states.STARTED]), ('exit', [bus.states.EXITING]), ] for method, states in flow: threading.Thread(target=f, args=(method,)).start() bus.wait(states) # The wait method MUST wait for the given state(s). assert bus.state in states, 'State %r not in %r' % (bus.state, states) @pytest.mark.xfail(CI_ON_MACOS, reason='continuous integration on macOS fails') def test_wait_publishes_periodically(bus): """Test that wait publishes each tick.""" callback = unittest.mock.MagicMock() bus.subscribe('main', callback) def set_start(): time.sleep(0.05) bus.start() threading.Thread(target=set_start).start() bus.wait(bus.states.STARTED, interval=0.01, channel='main') assert callback.call_count > 3 def test_block(bus, log_tracker): """Test that bus block waits for exiting.""" def f(): # pylint: disable=invalid-name time.sleep(0.2) bus.exit() def g(): # pylint: disable=invalid-name time.sleep(0.4) threading.Thread(target=f).start() threading.Thread(target=g).start() threads = [t for t in threading.enumerate() if not t.daemon] assert len(threads) == 3 bus.block() # The block method MUST wait for the EXITING state. assert bus.state == bus.states.EXITING # The block method MUST wait for ALL non-main, non-daemon threads to # finish. threads = [t for t in threading.enumerate() if not t.daemon] assert len(threads) == 1 # The last message will mention an indeterminable thread name; ignore # it expected_bus_messages = [ 'Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED', 'Waiting for child threads to terminate...', ] bus_msg_num = len(expected_bus_messages) # If the last message mentions an indeterminable thread name then ignore it assert log_tracker.log_entries[:bus_msg_num] == expected_bus_messages assert len(log_tracker.log_entries[bus_msg_num:]) <= 1, ( 'No more than one extra log line with the thread name expected' ) def test_start_with_callback(bus): """Test that callback fires on bus start.""" try: events = [] def f(*args, **kwargs): # pylint: disable=invalid-name events.append(('f', args, kwargs)) def g(): # pylint: disable=invalid-name events.append('g') bus.subscribe('start', g) bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) # Give wait() time to run f() time.sleep(0.2) # The callback method MUST wait for the STARTED state. assert bus.state == bus.states.STARTED # The callback method MUST run after all start methods. assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})] finally: bus.exit() def test_log(bus, log_tracker): """Test that bus messages and errors are logged.""" assert log_tracker.log_entries == [] # Try a normal message. expected = [] for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: bus.log(msg_) expected.append(msg_) assert log_tracker.log_entries == expected # Try an error message try: foo except NameError: bus.log('You are lost and gone forever', traceback=True) lastmsg = log_tracker.log_entries[-1] assert 'Traceback' in lastmsg and 'NameError' in lastmsg, ( 'Last log message %r did not contain ' 'the expected traceback.' % lastmsg ) else: pytest.fail('NameError was not raised as expected.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_caching.py0000644252176402575230000003406214307416152022071 0ustar00jaracoprimarygroupimport datetime from itertools import count import os import threading import time import urllib.parse import pytest import cherrypy from cherrypy.lib import httputil from cherrypy.test import helper curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) gif_bytes = ( b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;' ) class CacheTest(helper.CPWebCase): @staticmethod def setup_server(): @cherrypy.config(**{'tools.caching.on': True}) class Root: def __init__(self): self.counter = 0 self.control_counter = 0 self.longlock = threading.Lock() @cherrypy.expose def index(self): self.counter += 1 msg = 'visit #%s' % self.counter return msg @cherrypy.expose def control(self): self.control_counter += 1 return 'visit #%s' % self.control_counter @cherrypy.expose def a_gif(self): cherrypy.response.headers[ 'Last-Modified'] = httputil.HTTPDate() return gif_bytes @cherrypy.expose def long_process(self, seconds='1'): try: self.longlock.acquire() time.sleep(float(seconds)) finally: self.longlock.release() return 'success!' @cherrypy.expose def clear_cache(self, path): cherrypy._cache.store[cherrypy.request.base + path].clear() @cherrypy.config(**{ 'tools.caching.on': True, 'tools.response_headers.on': True, 'tools.response_headers.headers': [ ('Vary', 'Our-Varying-Header') ], }) class VaryHeaderCachingServer(object): def __init__(self): self.counter = count(1) @cherrypy.expose def index(self): return 'visit #%s' % next(self.counter) @cherrypy.config(**{ 'tools.expires.on': True, 'tools.expires.secs': 60, 'tools.staticdir.on': True, 'tools.staticdir.dir': 'static', 'tools.staticdir.root': curdir, }) class UnCached(object): @cherrypy.expose @cherrypy.config(**{'tools.expires.secs': 0}) def force(self): cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' self._cp_config['tools.expires.force'] = True self._cp_config['tools.expires.secs'] = 0 return 'being forceful' @cherrypy.expose def dynamic(self): cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' cherrypy.response.headers['Cache-Control'] = 'private' return 'D-d-d-dynamic!' @cherrypy.expose def cacheable(self): cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' return "Hi, I'm cacheable." @cherrypy.expose @cherrypy.config(**{'tools.expires.secs': 86400}) def specific(self): cherrypy.response.headers[ 'Etag'] = 'need_this_to_make_me_cacheable' return 'I am being specific' class Foo(object): pass @cherrypy.expose @cherrypy.config(**{'tools.expires.secs': Foo()}) def wrongtype(self): cherrypy.response.headers[ 'Etag'] = 'need_this_to_make_me_cacheable' return 'Woops' @cherrypy.config(**{ 'tools.gzip.mime_types': ['text/*', 'image/*'], 'tools.caching.on': True, 'tools.staticdir.on': True, 'tools.staticdir.dir': 'static', 'tools.staticdir.root': curdir }) class GzipStaticCache(object): pass cherrypy.tree.mount(Root()) cherrypy.tree.mount(UnCached(), '/expires') cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers') cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache') cherrypy.config.update({'tools.gzip.on': True}) def testCaching(self): elapsed = 0.0 for trial in range(10): self.getPage('/') # The response should be the same every time, # except for the Age response header. self.assertBody('visit #1') if trial != 0: age = int(self.assertHeader('Age')) assert age >= elapsed elapsed = age # POST, PUT, DELETE should not be cached. self.getPage('/', method='POST') self.assertBody('visit #2') # Because gzip is turned on, the Vary header should always Vary for # content-encoding self.assertHeader('Vary', 'Accept-Encoding') # The previous request should have invalidated the cache, # so this request will recalc the response. self.getPage('/', method='GET') self.assertBody('visit #3') # ...but this request should get the cached copy. self.getPage('/', method='GET') self.assertBody('visit #3') self.getPage('/', method='DELETE') self.assertBody('visit #4') # The previous request should have invalidated the cache, # so this request will recalc the response. self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) self.assertHeader('Content-Encoding', 'gzip') self.assertHeader('Vary') self.assertEqual( cherrypy.lib.encoding.decompress(self.body), b'visit #5') # Now check that a second request gets the gzip header and gzipped body # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped # response body was being gzipped a second time. self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) self.assertHeader('Content-Encoding', 'gzip') self.assertEqual( cherrypy.lib.encoding.decompress(self.body), b'visit #5') # Now check that a third request that doesn't accept gzip # skips the cache (because the 'Vary' header denies it). self.getPage('/', method='GET') self.assertNoHeader('Content-Encoding') self.assertBody('visit #6') def testVaryHeader(self): self.getPage('/varying_headers/') self.assertStatus('200 OK') self.assertHeaderItemValue('Vary', 'Our-Varying-Header') self.assertBody('visit #1') # Now check that different 'Vary'-fields don't evict each other. # This test creates 2 requests with different 'Our-Varying-Header' # and then tests if the first one still exists. self.getPage('/varying_headers/', headers=[('Our-Varying-Header', 'request 2')]) self.assertStatus('200 OK') self.assertBody('visit #2') self.getPage('/varying_headers/', headers=[('Our-Varying-Header', 'request 2')]) self.assertStatus('200 OK') self.assertBody('visit #2') self.getPage('/varying_headers/') self.assertStatus('200 OK') self.assertBody('visit #1') def testExpiresTool(self): # test setting an expires header self.getPage('/expires/specific') self.assertStatus('200 OK') self.assertHeader('Expires') # test exceptions for bad time values self.getPage('/expires/wrongtype') self.assertStatus(500) self.assertInBody('TypeError') # static content should not have "cache prevention" headers self.getPage('/expires/index.html') self.assertStatus('200 OK') self.assertNoHeader('Pragma') self.assertNoHeader('Cache-Control') self.assertHeader('Expires') # dynamic content that sets indicators should not have # "cache prevention" headers self.getPage('/expires/cacheable') self.assertStatus('200 OK') self.assertNoHeader('Pragma') self.assertNoHeader('Cache-Control') self.assertHeader('Expires') self.getPage('/expires/dynamic') self.assertBody('D-d-d-dynamic!') # the Cache-Control header should be untouched self.assertHeader('Cache-Control', 'private') self.assertHeader('Expires') # configure the tool to ignore indicators and replace existing headers self.getPage('/expires/force') self.assertStatus('200 OK') # This also gives us a chance to test 0 expiry with no other headers self.assertHeader('Pragma', 'no-cache') if cherrypy.server.protocol_version == 'HTTP/1.1': self.assertHeader('Cache-Control', 'no-cache, must-revalidate') self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') # static content should now have "cache prevention" headers self.getPage('/expires/index.html') self.assertStatus('200 OK') self.assertHeader('Pragma', 'no-cache') if cherrypy.server.protocol_version == 'HTTP/1.1': self.assertHeader('Cache-Control', 'no-cache, must-revalidate') self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') # the cacheable handler should now have "cache prevention" headers self.getPage('/expires/cacheable') self.assertStatus('200 OK') self.assertHeader('Pragma', 'no-cache') if cherrypy.server.protocol_version == 'HTTP/1.1': self.assertHeader('Cache-Control', 'no-cache, must-revalidate') self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') self.getPage('/expires/dynamic') self.assertBody('D-d-d-dynamic!') # dynamic sets Cache-Control to private but it should be # overwritten here ... self.assertHeader('Pragma', 'no-cache') if cherrypy.server.protocol_version == 'HTTP/1.1': self.assertHeader('Cache-Control', 'no-cache, must-revalidate') self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') def _assert_resp_len_and_enc_for_gzip(self, uri): """ Test that after querying gzipped content it's remains valid in cache and available non-gzipped as well. """ ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')] content_len = None for _ in range(3): self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS) if content_len is not None: # all requests should get the same length self.assertHeader('Content-Length', content_len) self.assertHeader('Content-Encoding', 'gzip') content_len = dict(self.headers)['Content-Length'] # check that we can still get non-gzipped version self.getPage(uri, method='GET') self.assertNoHeader('Content-Encoding') # non-gzipped version should have a different content length self.assertNoHeaderItemValue('Content-Length', content_len) def testGzipStaticCache(self): """Test that cache and gzip tools play well together when both enabled. Ref GitHub issue #1190. """ GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}' resource_files = ('index.html', 'dirback.jpg') for f in resource_files: uri = GZIP_STATIC_CACHE_TMPL.format(f) self._assert_resp_len_and_enc_for_gzip(uri) def testLastModified(self): self.getPage('/a.gif') self.assertStatus(200) self.assertBody(gif_bytes) lm1 = self.assertHeader('Last-Modified') # this request should get the cached copy. self.getPage('/a.gif') self.assertStatus(200) self.assertBody(gif_bytes) self.assertHeader('Age') lm2 = self.assertHeader('Last-Modified') self.assertEqual(lm1, lm2) # this request should match the cached copy, but raise 304. self.getPage('/a.gif', [('If-Modified-Since', lm1)]) self.assertStatus(304) self.assertNoHeader('Last-Modified') if not getattr(cherrypy.server, 'using_apache', False): self.assertHeader('Age') @pytest.mark.xfail(reason='#1536') def test_antistampede(self): SECONDS = 4 slow_url = '/long_process?seconds={SECONDS}'.format(**locals()) # We MUST make an initial synchronous request in order to create the # AntiStampedeCache object, and populate its selecting_headers, # before the actual stampede. self.getPage(slow_url) self.assertBody('success!') path = urllib.parse.quote(slow_url, safe='') self.getPage('/clear_cache?path=' + path) self.assertStatus(200) start = datetime.datetime.now() def run(): self.getPage(slow_url) # The response should be the same every time self.assertBody('success!') ts = [threading.Thread(target=run) for i in range(100)] for t in ts: t.start() for t in ts: t.join() finish = datetime.datetime.now() # Allow for overhead, two seconds for slow hosts allowance = SECONDS + 2 self.assertEqualDates(start, finish, seconds=allowance) def test_cache_control(self): self.getPage('/control') self.assertBody('visit #1') self.getPage('/control') self.assertBody('visit #1') self.getPage('/control', headers=[('Cache-Control', 'no-cache')]) self.assertBody('visit #2') self.getPage('/control') self.assertBody('visit #2') self.getPage('/control', headers=[('Pragma', 'no-cache')]) self.assertBody('visit #3') self.getPage('/control') self.assertBody('visit #3') time.sleep(1) self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) self.assertBody('visit #4') self.getPage('/control') self.assertBody('visit #4') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_config.py0000644252176402575230000002120414307416152021734 0ustar00jaracoprimarygroup"""Tests for the CherryPy configuration system.""" import io import os import sys import unittest import cherrypy from cherrypy.test import helper localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) def StringIOFromNative(x): return io.StringIO(str(x)) def setup_server(): @cherrypy.config(foo='this', bar='that') class Root: def __init__(self): cherrypy.config.namespaces['db'] = self.db_namespace def db_namespace(self, k, v): if k == 'scheme': self.db = v @cherrypy.expose(alias=('global_', 'xyz')) def index(self, key): return cherrypy.request.config.get(key, 'None') @cherrypy.expose def repr(self, key): return repr(cherrypy.request.config.get(key, None)) @cherrypy.expose def dbscheme(self): return self.db @cherrypy.expose @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']}) def plain(self, x): return x favicon_ico = cherrypy.tools.staticfile.handler( filename=os.path.join(localDir, '../favicon.ico')) @cherrypy.config(foo='this2', baz='that2') class Foo: @cherrypy.expose def index(self, key): return cherrypy.request.config.get(key, 'None') nex = index @cherrypy.expose @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) def silly(self): return 'Hello world' # Test the expose and config decorators @cherrypy.config(foo='this3', **{'bax': 'this4'}) @cherrypy.expose def bar(self, key): return repr(cherrypy.request.config.get(key, None)) class Another: @cherrypy.expose def index(self, key): return str(cherrypy.request.config.get(key, 'None')) def raw_namespace(key, value): if key == 'input.map': handler = cherrypy.request.handler def wrapper(): params = cherrypy.request.params for name, coercer in value.copy().items(): try: params[name] = coercer(params[name]) except KeyError: pass return handler() cherrypy.request.handler = wrapper elif key == 'output': handler = cherrypy.request.handler def wrapper(): # 'value' is a type (like int or str). return value(handler()) cherrypy.request.handler = wrapper @cherrypy.config(**{'raw.output': repr}) class Raw: @cherrypy.expose @cherrypy.config(**{'raw.input.map': {'num': int}}) def incr(self, num): return num + 1 ioconf = StringIOFromNative(""" [/] neg: -1234 filename: os.path.join(sys.prefix, "hello.py") thing1: cherrypy.lib.httputil.response_codes[404] thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 complex: 3+2j mul: 6*3 ones: "11" twos: "22" stradd: %%(ones)s + %%(twos)s + "33" [/favicon.ico] tools.staticfile.filename = %r """ % os.path.join(localDir, 'static/dirback.jpg')) root = Root() root.foo = Foo() root.raw = Raw() app = cherrypy.tree.mount(root, config=ioconf) app.request_class.namespaces['raw'] = raw_namespace cherrypy.tree.mount(Another(), '/another') cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', 'db.scheme': r'sqlite///memory', }) # Client-side code # class ConfigTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def testConfig(self): tests = [ ('/', 'nex', 'None'), ('/', 'foo', 'this'), ('/', 'bar', 'that'), ('/xyz', 'foo', 'this'), ('/foo/', 'foo', 'this2'), ('/foo/', 'bar', 'that'), ('/foo/', 'bax', 'None'), ('/foo/bar', 'baz', "'that2'"), ('/foo/nex', 'baz', 'that2'), # If 'foo' == 'this', then the mount point '/another' leaks into # '/'. ('/another/', 'foo', 'None'), ] for path, key, expected in tests: self.getPage(path + '?key=' + key) self.assertBody(expected) expectedconf = { # From CP defaults 'tools.log_headers.on': False, 'tools.log_tracebacks.on': True, 'request.show_tracebacks': True, 'log.screen': False, 'environment': 'test_suite', 'engine.autoreload.on': False, # From global config 'luxuryyacht': 'throatwobblermangrove', # From Root._cp_config 'bar': 'that', # From Foo._cp_config 'baz': 'that2', # From Foo.bar._cp_config 'foo': 'this3', 'bax': 'this4', } for key, expected in expectedconf.items(): self.getPage('/foo/bar?key=' + key) self.assertBody(repr(expected)) def testUnrepr(self): self.getPage('/repr?key=neg') self.assertBody('-1234') self.getPage('/repr?key=filename') self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) self.getPage('/repr?key=thing1') self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) if not getattr(cherrypy.server, 'using_apache', False): # The object ID's won't match up when using Apache, since the # server and client are running in different processes. self.getPage('/repr?key=thing2') from cherrypy.tutorial import thing2 self.assertBody(repr(thing2)) self.getPage('/repr?key=complex') self.assertBody('(3+2j)') self.getPage('/repr?key=mul') self.assertBody('18') self.getPage('/repr?key=stradd') self.assertBody(repr('112233')) def testRespNamespaces(self): self.getPage('/foo/silly') self.assertHeader('X-silly', 'sillyval') self.assertBody('Hello world') def testCustomNamespaces(self): self.getPage('/raw/incr?num=12') self.assertBody('13') self.getPage('/dbscheme') self.assertBody(r'sqlite///memory') def testHandlerToolConfigOverride(self): # Assert that config overrides tool constructor args. Above, we set # the favicon in the page handler to be '../favicon.ico', # but then overrode it in config to be './static/dirback.jpg'. self.getPage('/favicon.ico') with open(os.path.join(localDir, 'static/dirback.jpg'), 'rb') as tf: self.assertBody(tf.read()) def test_request_body_namespace(self): self.getPage('/plain', method='POST', headers=[ ('Content-Type', 'application/x-www-form-urlencoded'), ('Content-Length', '13')], body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00') self.assertBody('abc') class VariableSubstitutionTests(unittest.TestCase): setup_server = staticmethod(setup_server) def test_config(self): from textwrap import dedent # variable substitution with [DEFAULT] conf = dedent(""" [DEFAULT] dir = "/some/dir" my.dir = %(dir)s + "/sub" [my] my.dir = %(dir)s + "/my/dir" my.dir2 = %(my.dir)s + '/dir2' """) fp = StringIOFromNative(conf) cherrypy.config.update(fp) self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir') self.assertEqual(cherrypy.config['my'] ['my.dir2'], '/some/dir/my/dir/dir2') class CallablesInConfigTest(unittest.TestCase): setup_server = staticmethod(setup_server) def test_call_with_literal_dict(self): from textwrap import dedent conf = dedent(""" [my] value = dict(**{'foo': 'bar'}) """) fp = StringIOFromNative(conf) cherrypy.config.update(fp) self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'}) def test_call_with_kwargs(self): from textwrap import dedent conf = dedent(""" [my] value = dict(foo="buzz", **cherrypy._test_dict) """) test_dict = { 'foo': 'bar', 'bar': 'foo', 'fizz': 'buzz' } cherrypy._test_dict = test_dict fp = StringIOFromNative(conf) cherrypy.config.update(fp) test_dict['foo'] = 'buzz' self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz') self.assertEqual(cherrypy.config['my']['value'], test_dict) del cherrypy._test_dict ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_config_server.py0000644252176402575230000000770514307416152023334 0ustar00jaracoprimarygroup"""Tests for the CherryPy configuration system.""" import os import cherrypy from cherrypy.test import helper localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) # Client-side code # class ServerConfigTests(helper.CPWebCase): @staticmethod def setup_server(): class Root: @cherrypy.expose def index(self): return cherrypy.request.wsgi_environ['SERVER_PORT'] @cherrypy.expose def upload(self, file): return 'Size: %s' % len(file.file.read()) @cherrypy.expose @cherrypy.config(**{'request.body.maxbytes': 100}) def tinyupload(self): return cherrypy.request.body.read() cherrypy.tree.mount(Root()) cherrypy.config.update({ 'server.socket_host': '0.0.0.0', 'server.socket_port': 9876, 'server.max_request_body_size': 200, 'server.max_request_header_size': 500, 'server.socket_timeout': 0.5, # Test explicit server.instance 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', 'server.2.socket_port': 9877, # Test non-numeric # Also test default server.instance = builtin server 'server.yetanother.socket_port': 9878, }) PORT = 9876 def testBasicConfig(self): self.getPage('/') self.assertBody(str(self.PORT)) def testAdditionalServers(self): if self.scheme == 'https': return self.skip('not available under ssl') self.PORT = 9877 self.getPage('/') self.assertBody(str(self.PORT)) self.PORT = 9878 self.getPage('/') self.assertBody(str(self.PORT)) def testMaxRequestSizePerHandler(self): if getattr(cherrypy.server, 'using_apache', False): return self.skip('skipped due to known Apache differences... ') self.getPage('/tinyupload', method='POST', headers=[('Content-Type', 'text/plain'), ('Content-Length', '100')], body='x' * 100) self.assertStatus(200) self.assertBody('x' * 100) self.getPage('/tinyupload', method='POST', headers=[('Content-Type', 'text/plain'), ('Content-Length', '101')], body='x' * 101) self.assertStatus(413) def testMaxRequestSize(self): if getattr(cherrypy.server, 'using_apache', False): return self.skip('skipped due to known Apache differences... ') for size in (500, 5000, 50000): self.getPage('/', headers=[('From', 'x' * 500)]) self.assertStatus(413) # Test for https://github.com/cherrypy/cherrypy/issues/421 # (Incorrect border condition in readline of SizeCheckWrapper). # This hangs in rev 891 and earlier. lines256 = 'x' * 248 self.getPage('/', headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), ('From', lines256)]) # Test upload cd = ( 'Content-Disposition: form-data; ' 'name="file"; ' 'filename="hello.txt"' ) body = '\r\n'.join([ '--x', cd, 'Content-Type: text/plain', '', '%s', '--x--']) partlen = 200 - len(body) b = body % ('x' * partlen) h = [('Content-type', 'multipart/form-data; boundary=x'), ('Content-Length', '%s' % len(b))] self.getPage('/upload', h, 'POST', b) self.assertBody('Size: %d' % partlen) b = body % ('x' * 200) h = [('Content-type', 'multipart/form-data; boundary=x'), ('Content-Length', '%s' % len(b))] self.getPage('/upload', h, 'POST', b) self.assertStatus(413) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_conn.py0000644252176402575230000007403014307416152021431 0ustar00jaracoprimarygroup"""Tests for TCP connection handling, including proper and timely close.""" import errno import socket import sys import time import urllib.parse from http.client import BadStatusLine, HTTPConnection, NotConnected from cheroot.test import webtest import cherrypy from cherrypy._cpcompat import HTTPSConnection, ntob, tonative from cherrypy.test import helper timeout = 1 pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' def setup_server(): def raise500(): raise cherrypy.HTTPError(500) class Root: @cherrypy.expose def index(self): return pov page1 = index page2 = index page3 = index @cherrypy.expose def hello(self): return 'Hello, world!' @cherrypy.expose def timeout(self, t): return str(cherrypy.server.httpserver.timeout) @cherrypy.expose @cherrypy.config(**{'response.stream': True}) def stream(self, set_cl=False): if set_cl: cherrypy.response.headers['Content-Length'] = 10 def content(): for x in range(10): yield str(x) return content() @cherrypy.expose def error(self, code=500): raise cherrypy.HTTPError(code) @cherrypy.expose def upload(self): if not cherrypy.request.method == 'POST': raise AssertionError("'POST' != request.method %r" % cherrypy.request.method) return "thanks for '%s'" % cherrypy.request.body.read() @cherrypy.expose def custom(self, response_code): cherrypy.response.status = response_code return 'Code = %s' % response_code @cherrypy.expose @cherrypy.config(**{'hooks.on_start_resource': raise500}) def err_before_read(self): return 'ok' @cherrypy.expose def one_megabyte_of_a(self): return ['a' * 1024] * 1024 @cherrypy.expose # Turn off the encoding tool so it doens't collapse # our response body and reclaculate the Content-Length. @cherrypy.config(**{'tools.encode.on': False}) def custom_cl(self, body, cl): cherrypy.response.headers['Content-Length'] = cl if not isinstance(body, list): body = [body] newbody = [] for chunk in body: if isinstance(chunk, str): chunk = chunk.encode('ISO-8859-1') newbody.append(chunk) return newbody cherrypy.tree.mount(Root()) cherrypy.config.update({ 'server.max_request_body_size': 1001, 'server.socket_timeout': timeout, }) class ConnectionCloseTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def test_HTTP11(self): if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' self.persistent = True # Make the first request and assert there's no "Connection: close". self.getPage('/') self.assertStatus('200 OK') self.assertBody(pov) self.assertNoHeader('Connection') # Make another request on the same connection. self.getPage('/page1') self.assertStatus('200 OK') self.assertBody(pov) self.assertNoHeader('Connection') # Test client-side close. self.getPage('/page2', headers=[('Connection', 'close')]) self.assertStatus('200 OK') self.assertBody(pov) self.assertHeader('Connection', 'close') # Make another request on the same connection, which should error. self.assertRaises(NotConnected, self.getPage, '/') def test_Streaming_no_len(self): try: self._streaming(set_cl=False) finally: try: self.HTTP_CONN.close() except (TypeError, AttributeError): pass def test_Streaming_with_len(self): try: self._streaming(set_cl=True) finally: try: self.HTTP_CONN.close() except (TypeError, AttributeError): pass def _streaming(self, set_cl): if cherrypy.server.protocol_version == 'HTTP/1.1': self.PROTOCOL = 'HTTP/1.1' self.persistent = True # Make the first request and assert there's no "Connection: close". self.getPage('/') self.assertStatus('200 OK') self.assertBody(pov) self.assertNoHeader('Connection') # Make another, streamed request on the same connection. if set_cl: # When a Content-Length is provided, the content should stream # without closing the connection. self.getPage('/stream?set_cl=Yes') self.assertHeader('Content-Length') self.assertNoHeader('Connection', 'close') self.assertNoHeader('Transfer-Encoding') self.assertStatus('200 OK') self.assertBody('0123456789') else: # When no Content-Length response header is provided, # streamed output will either close the connection, or use # chunked encoding, to determine transfer-length. self.getPage('/stream') self.assertNoHeader('Content-Length') self.assertStatus('200 OK') self.assertBody('0123456789') chunked_response = False for k, v in self.headers: if k.lower() == 'transfer-encoding': if str(v) == 'chunked': chunked_response = True if chunked_response: self.assertNoHeader('Connection', 'close') else: self.assertHeader('Connection', 'close') # Make another request on the same connection, which should # error. self.assertRaises(NotConnected, self.getPage, '/') # Try HEAD. See # https://github.com/cherrypy/cherrypy/issues/864. self.getPage('/stream', method='HEAD') self.assertStatus('200 OK') self.assertBody('') self.assertNoHeader('Transfer-Encoding') else: self.PROTOCOL = 'HTTP/1.0' self.persistent = True # Make the first request and assert Keep-Alive. self.getPage('/', headers=[('Connection', 'Keep-Alive')]) self.assertStatus('200 OK') self.assertBody(pov) self.assertHeader('Connection', 'Keep-Alive') # Make another, streamed request on the same connection. if set_cl: # When a Content-Length is provided, the content should # stream without closing the connection. self.getPage('/stream?set_cl=Yes', headers=[('Connection', 'Keep-Alive')]) self.assertHeader('Content-Length') self.assertHeader('Connection', 'Keep-Alive') self.assertNoHeader('Transfer-Encoding') self.assertStatus('200 OK') self.assertBody('0123456789') else: # When a Content-Length is not provided, # the server should close the connection. self.getPage('/stream', headers=[('Connection', 'Keep-Alive')]) self.assertStatus('200 OK') self.assertBody('0123456789') self.assertNoHeader('Content-Length') self.assertNoHeader('Connection', 'Keep-Alive') self.assertNoHeader('Transfer-Encoding') # Make another request on the same connection, which should # error. self.assertRaises(NotConnected, self.getPage, '/') def test_HTTP10_KeepAlive(self): self.PROTOCOL = 'HTTP/1.0' if self.scheme == 'https': self.HTTP_CONN = HTTPSConnection else: self.HTTP_CONN = HTTPConnection # Test a normal HTTP/1.0 request. self.getPage('/page2') self.assertStatus('200 OK') self.assertBody(pov) # Apache, for example, may emit a Connection header even for HTTP/1.0 # self.assertNoHeader("Connection") # Test a keep-alive HTTP/1.0 request. self.persistent = True self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) self.assertStatus('200 OK') self.assertBody(pov) self.assertHeader('Connection', 'Keep-Alive') # Remove the keep-alive header again. self.getPage('/page3') self.assertStatus('200 OK') self.assertBody(pov) # Apache, for example, may emit a Connection header even for HTTP/1.0 # self.assertNoHeader("Connection") class PipelineTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def test_HTTP11_Timeout(self): # If we timeout without sending any data, # the server will close the conn with a 408. if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' # Connect but send nothing. self.persistent = True conn = self.HTTP_CONN conn.auto_open = False conn.connect() # Wait for our socket timeout time.sleep(timeout * 2) # The request should have returned 408 already. response = conn.response_class(conn.sock, method='GET') response.begin() self.assertEqual(response.status, 408) conn.close() # Connect but send half the headers only. self.persistent = True conn = self.HTTP_CONN conn.auto_open = False conn.connect() conn.send(b'GET /hello HTTP/1.1') conn.send(('Host: %s' % self.HOST).encode('ascii')) # Wait for our socket timeout time.sleep(timeout * 2) # The conn should have already sent 408. response = conn.response_class(conn.sock, method='GET') response.begin() self.assertEqual(response.status, 408) conn.close() def test_HTTP11_Timeout_after_request(self): # If we timeout after at least one request has succeeded, # the server will close the conn without 408. if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' # Make an initial request self.persistent = True conn = self.HTTP_CONN conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) conn.putheader('Host', self.HOST) conn.endheaders() response = conn.response_class(conn.sock, method='GET') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(str(timeout)) # Make a second request on the same socket conn._output(b'GET /hello HTTP/1.1') conn._output(ntob('Host: %s' % self.HOST, 'ascii')) conn._send_output() response = conn.response_class(conn.sock, method='GET') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody('Hello, world!') # Wait for our socket timeout time.sleep(timeout * 2) # Make another request on the same socket, which should error conn._output(b'GET /hello HTTP/1.1') conn._output(ntob('Host: %s' % self.HOST, 'ascii')) conn._send_output() response = conn.response_class(conn.sock, method='GET') msg = ( "Writing to timed out socket didn't fail as it should have: %s") try: response.begin() except Exception: if not isinstance(sys.exc_info()[1], (socket.error, BadStatusLine)): self.fail(msg % sys.exc_info()[1]) else: if response.status != 408: self.fail(msg % response.read()) conn.close() # Make another request on a new socket, which should work self.persistent = True conn = self.HTTP_CONN conn.putrequest('GET', '/', skip_host=True) conn.putheader('Host', self.HOST) conn.endheaders() response = conn.response_class(conn.sock, method='GET') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(pov) # Make another request on the same socket, # but timeout on the headers conn.send(b'GET /hello HTTP/1.1') # Wait for our socket timeout time.sleep(timeout * 2) response = conn.response_class(conn.sock, method='GET') try: response.begin() except Exception: if not isinstance(sys.exc_info()[1], (socket.error, BadStatusLine)): self.fail(msg % sys.exc_info()[1]) else: if response.status != 408: self.fail(msg % response.read()) conn.close() # Retry the request on a new connection, which should work self.persistent = True conn = self.HTTP_CONN conn.putrequest('GET', '/', skip_host=True) conn.putheader('Host', self.HOST) conn.endheaders() response = conn.response_class(conn.sock, method='GET') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(pov) conn.close() def test_HTTP11_pipelining(self): if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' # Test pipelining. httplib doesn't support this directly. self.persistent = True conn = self.HTTP_CONN # Put request 1 conn.putrequest('GET', '/hello', skip_host=True) conn.putheader('Host', self.HOST) conn.endheaders() for trial in range(5): # Put next request conn._output(b'GET /hello HTTP/1.1') conn._output(ntob('Host: %s' % self.HOST, 'ascii')) conn._send_output() # Retrieve previous response response = conn.response_class(conn.sock, method='GET') # there is a bug in python3 regarding the buffering of # ``conn.sock``. Until that bug get's fixed we will # monkey patch the ``response`` instance. # https://bugs.python.org/issue23377 response.fp = conn.sock.makefile('rb', 0) response.begin() body = response.read(13) self.assertEqual(response.status, 200) self.assertEqual(body, b'Hello, world!') # Retrieve final response response = conn.response_class(conn.sock, method='GET') response.begin() body = response.read() self.assertEqual(response.status, 200) self.assertEqual(body, b'Hello, world!') conn.close() def test_100_Continue(self): if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' self.persistent = True conn = self.HTTP_CONN # Try a page without an Expect request header first. # Note that httplib's response.begin automatically ignores # 100 Continue responses, so we must manually check for it. try: conn.putrequest('POST', '/upload', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Type', 'text/plain') conn.putheader('Content-Length', '4') conn.endheaders() conn.send(ntob("d'oh")) response = conn.response_class(conn.sock, method='POST') version, status, reason = response._read_status() self.assertNotEqual(status, 100) finally: conn.close() # Now try a page with an Expect header... try: conn.connect() conn.putrequest('POST', '/upload', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Type', 'text/plain') conn.putheader('Content-Length', '17') conn.putheader('Expect', '100-continue') conn.endheaders() response = conn.response_class(conn.sock, method='POST') # ...assert and then skip the 100 response version, status, reason = response._read_status() self.assertEqual(status, 100) while True: line = response.fp.readline().strip() if line: self.fail( '100 Continue should not output any headers. Got %r' % line) else: break # ...send the body body = b'I am a small file' conn.send(body) # ...get the final response response.begin() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(200) self.assertBody("thanks for '%s'" % body) finally: conn.close() class ConnectionTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def test_readall_or_close(self): if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' if self.scheme == 'https': self.HTTP_CONN = HTTPSConnection else: self.HTTP_CONN = HTTPConnection # Test a max of 0 (the default) and then reset to what it was above. old_max = cherrypy.server.max_request_body_size for new_max in (0, old_max): cherrypy.server.max_request_body_size = new_max self.persistent = True conn = self.HTTP_CONN # Get a POST page with an error conn.putrequest('POST', '/err_before_read', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Type', 'text/plain') conn.putheader('Content-Length', '1000') conn.putheader('Expect', '100-continue') conn.endheaders() response = conn.response_class(conn.sock, method='POST') # ...assert and then skip the 100 response version, status, reason = response._read_status() self.assertEqual(status, 100) while True: skip = response.fp.readline().strip() if not skip: break # ...send the body conn.send(ntob('x' * 1000)) # ...get the final response response.begin() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(500) # Now try a working page with an Expect header... conn._output(b'POST /upload HTTP/1.1') conn._output(ntob('Host: %s' % self.HOST, 'ascii')) conn._output(b'Content-Type: text/plain') conn._output(b'Content-Length: 17') conn._output(b'Expect: 100-continue') conn._send_output() response = conn.response_class(conn.sock, method='POST') # ...assert and then skip the 100 response version, status, reason = response._read_status() self.assertEqual(status, 100) while True: skip = response.fp.readline().strip() if not skip: break # ...send the body body = b'I am a small file' conn.send(body) # ...get the final response response.begin() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(200) self.assertBody("thanks for '%s'" % body) conn.close() def test_No_Message_Body(self): if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() self.PROTOCOL = 'HTTP/1.1' # Set our HTTP_CONN to an instance so it persists between requests. self.persistent = True # Make the first request and assert there's no "Connection: close". self.getPage('/') self.assertStatus('200 OK') self.assertBody(pov) self.assertNoHeader('Connection') # Make a 204 request on the same connection. self.getPage('/custom/204') self.assertStatus(204) self.assertNoHeader('Content-Length') self.assertBody('') self.assertNoHeader('Connection') # Make a 304 request on the same connection. self.getPage('/custom/304') self.assertStatus(304) self.assertNoHeader('Content-Length') self.assertBody('') self.assertNoHeader('Connection') def test_Chunked_Encoding(self): if cherrypy.server.protocol_version != 'HTTP/1.1': return self.skip() if (hasattr(self, 'harness') and 'modpython' in self.harness.__class__.__name__.lower()): # mod_python forbids chunked encoding return self.skip() self.PROTOCOL = 'HTTP/1.1' # Set our HTTP_CONN to an instance so it persists between requests. self.persistent = True conn = self.HTTP_CONN # Try a normal chunked request (with extensions) body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' 'Content-Type: application/json\r\n' '\r\n') conn.putrequest('POST', '/upload', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Transfer-Encoding', 'chunked') conn.putheader('Trailer', 'Content-Type') # Note that this is somewhat malformed: # we shouldn't be sending Content-Length. # RFC 2616 says the server should ignore it. conn.putheader('Content-Length', '3') conn.endheaders() conn.send(body) response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus('200 OK') self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy') # Try a chunked request that exceeds server.max_request_body_size. # Note that the delimiters and trailer are included. body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n') conn.putrequest('POST', '/upload', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Transfer-Encoding', 'chunked') conn.putheader('Content-Type', 'text/plain') # Chunked requests don't need a content-length # # conn.putheader("Content-Length", len(body)) conn.endheaders() conn.send(body) response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(413) conn.close() def test_Content_Length_in(self): # Try a non-chunked request where Content-Length exceeds # server.max_request_body_size. Assert error before body send. self.persistent = True conn = self.HTTP_CONN conn.putrequest('POST', '/upload', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Type', 'text/plain') conn.putheader('Content-Length', '9999') conn.endheaders() response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(413) self.assertBody('The entity sent with the request exceeds ' 'the maximum allowed bytes.') conn.close() def test_Content_Length_out_preheaders(self): # Try a non-chunked response where Content-Length is less than # the actual bytes in the response body. self.persistent = True conn = self.HTTP_CONN conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5', skip_host=True) conn.putheader('Host', self.HOST) conn.endheaders() response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(500) self.assertBody( 'The requested resource returned more bytes than the ' 'declared Content-Length.') conn.close() def test_Content_Length_out_postheaders(self): # Try a non-chunked response where Content-Length is less than # the actual bytes in the response body. self.persistent = True conn = self.HTTP_CONN conn.putrequest( 'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5', skip_host=True) conn.putheader('Host', self.HOST) conn.endheaders() response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(200) self.assertBody('I too') conn.close() def test_598(self): tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' url = tmpl.format( scheme=self.scheme, host=self.HOST, port=self.PORT, ) remote_data_conn = urllib.request.urlopen(url) buf = remote_data_conn.read(512) time.sleep(timeout * 0.6) remaining = (1024 * 1024) - 512 while remaining: data = remote_data_conn.read(remaining) if not data: break else: buf += data remaining -= len(data) self.assertEqual(len(buf), 1024 * 1024) self.assertEqual(buf, ntob('a' * 1024 * 1024)) self.assertEqual(remaining, 0) remote_data_conn.close() def setup_upload_server(): class Root: @cherrypy.expose def upload(self): if not cherrypy.request.method == 'POST': raise AssertionError("'POST' != request.method %r" % cherrypy.request.method) return "thanks for '%s'" % tonative(cherrypy.request.body.read()) cherrypy.tree.mount(Root()) cherrypy.config.update({ 'server.max_request_body_size': 1001, 'server.socket_timeout': 10, 'server.accepted_queue_size': 5, 'server.accepted_queue_timeout': 0.1, }) reset_names = 'ECONNRESET', 'WSAECONNRESET' socket_reset_errors = [ getattr(errno, name) for name in reset_names if hasattr(errno, name) ] 'reset error numbers available on this platform' socket_reset_errors += [ # Python 3.5 raises an http.client.RemoteDisconnected # with this message 'Remote end closed connection without response', ] class LimitedRequestQueueTests(helper.CPWebCase): setup_server = staticmethod(setup_upload_server) def test_queue_full(self): conns = [] overflow_conn = None try: # Make 15 initial requests and leave them open, which should use # all of wsgiserver's WorkerThreads and fill its Queue. for i in range(15): conn = self.HTTP_CONN(self.HOST, self.PORT) conn.putrequest('POST', '/upload', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Type', 'text/plain') conn.putheader('Content-Length', '4') conn.endheaders() conns.append(conn) # Now try a 16th conn, which should be closed by the # server immediately. overflow_conn = self.HTTP_CONN(self.HOST, self.PORT) # Manually connect since httplib won't let us set a timeout for res in socket.getaddrinfo(self.HOST, self.PORT, 0, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res overflow_conn.sock = socket.socket(af, socktype, proto) overflow_conn.sock.settimeout(5) overflow_conn.sock.connect(sa) break overflow_conn.putrequest('GET', '/', skip_host=True) overflow_conn.putheader('Host', self.HOST) overflow_conn.endheaders() response = overflow_conn.response_class( overflow_conn.sock, method='GET', ) try: response.begin() except socket.error as exc: if exc.args[0] in socket_reset_errors: pass # Expected. else: tmpl = ( 'Overflow conn did not get RST. ' 'Got {exc.args!r} instead' ) raise AssertionError(tmpl.format(**locals())) except BadStatusLine: # This is a special case in OS X. Linux and Windows will # RST correctly. assert sys.platform == 'darwin' else: raise AssertionError('Overflow conn did not get RST ') finally: for conn in conns: conn.send(b'done') response = conn.response_class(conn.sock, method='POST') response.begin() self.body = response.read() self.assertBody("thanks for 'done'") self.assertEqual(response.status, 200) conn.close() if overflow_conn: overflow_conn.close() class BadRequestTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def test_No_CRLF(self): self.persistent = True conn = self.HTTP_CONN conn.send(b'GET /hello HTTP/1.1\n\n') response = conn.response_class(conn.sock, method='GET') response.begin() self.body = response.read() self.assertBody('HTTP requires CRLF terminators') conn.close() conn.connect() conn.send(b'GET /hello HTTP/1.1\r\n\n') response = conn.response_class(conn.sock, method='GET') response.begin() self.body = response.read() self.assertBody('HTTP requires CRLF terminators') conn.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/cherrypy/test/test_core.py0000644252176402575230000007327314307416152021434 0ustar00jaracoprimarygroup# coding: utf-8 """Basic tests for the CherryPy core: request handling.""" import os import sys import types import cherrypy from cherrypy._cpcompat import ntou from cherrypy import _cptools, tools from cherrypy.lib import httputil, static from cherrypy.test._test_decorators import ExposeExamples from cherrypy.test import helper localDir = os.path.dirname(__file__) favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico') # Client-side code # class CoreRequestHandlingTest(helper.CPWebCase): @staticmethod def setup_server(): class Root: @cherrypy.expose def index(self): return 'hello' favicon_ico = tools.staticfile.handler(filename=favicon_path) @cherrypy.expose def defct(self, newct): newct = 'text/%s' % newct cherrypy.config.update({'tools.response_headers.on': True, 'tools.response_headers.headers': [('Content-Type', newct)]}) @cherrypy.expose def baseurl(self, path_info, relative=None): return cherrypy.url(path_info, relative=bool(relative)) root = Root() root.expose_dec = ExposeExamples() class TestType(type): """Metaclass which automatically exposes all functions in each subclass, and adds an instance of the subclass as an attribute of root. """ def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) for value in dct.values(): if isinstance(value, types.FunctionType): value.exposed = True setattr(root, name.lower(), cls()) Test = TestType('Test', (object, ), {}) @cherrypy.config(**{'tools.trailing_slash.on': False}) class URL(Test): def index(self, path_info, relative=None): if relative != 'server': relative = bool(relative) return cherrypy.url(path_info, relative=relative) def leaf(self, path_info, relative=None): if relative != 'server': relative = bool(relative) return cherrypy.url(path_info, relative=relative) def qs(self, qs): return cherrypy.url(qs=qs) def log_status(): Status.statuses.append(cherrypy.response.status) cherrypy.tools.log_status = cherrypy.Tool( 'on_end_resource', log_status) class Status(Test): def index(self): return 'normal' def blank(self): cherrypy.response.status = '' # According to RFC 2616, new status codes are OK as long as they # are between 100 and 599. # Here is an illegal code... def illegal(self): cherrypy.response.status = 781 return 'oops' # ...and here is an unknown but legal code. def unknown(self): cherrypy.response.status = '431 My custom error' return 'funky' # Non-numeric code def bad(self): cherrypy.response.status = 'error' return 'bad news' statuses = [] @cherrypy.config(**{'tools.log_status.on': True}) def on_end_resource_stage(self): return repr(self.statuses) class Redirect(Test): @cherrypy.config(**{ 'tools.err_redirect.on': True, 'tools.err_redirect.url': '/errpage', 'tools.err_redirect.internal': False, }) class Error: @cherrypy.expose def index(self): raise NameError('redirect_test') error = Error() def index(self): return 'child' def custom(self, url, code): raise cherrypy.HTTPRedirect(url, code) @cherrypy.config(**{'tools.trailing_slash.extra': True}) def by_code(self, code): raise cherrypy.HTTPRedirect('somewhere%20else', code) def nomodify(self): raise cherrypy.HTTPRedirect('', 304) def proxy(self): raise cherrypy.HTTPRedirect('proxy', 305) def stringify(self): return str(cherrypy.HTTPRedirect('/')) def fragment(self, frag): raise cherrypy.HTTPRedirect('/some/url#%s' % frag) def url_with_quote(self): raise cherrypy.HTTPRedirect("/some\"url/that'we/want") def url_with_xss(self): raise cherrypy.HTTPRedirect( "/someurl/that'we/want") def url_with_unicode(self): raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) def login_redir(): if not getattr(cherrypy.request, 'login', None): raise cherrypy.InternalRedirect('/internalredirect/login') tools.login_redir = _cptools.Tool('before_handler', login_redir) def redir_custom(): raise cherrypy.InternalRedirect('/internalredirect/custom_err') class InternalRedirect(Test): def index(self): raise cherrypy.InternalRedirect('/') @cherrypy.expose @cherrypy.config(**{'hooks.before_error_response': redir_custom}) def choke(self): return 3 / 0 def relative(self, a, b): raise cherrypy.InternalRedirect('cousin?t=6') def cousin(self, t): assert cherrypy.request.prev.closed return cherrypy.request.prev.query_string def petshop(self, user_id): if user_id == 'parrot': # Trade it for a slug when redirecting raise cherrypy.InternalRedirect( '/image/getImagesByUser?user_id=slug') elif user_id == 'terrier': # Trade it for a fish when redirecting raise cherrypy.InternalRedirect( '/image/getImagesByUser?user_id=fish') else: # This should pass the user_id through to getImagesByUser raise cherrypy.InternalRedirect( '/image/getImagesByUser?user_id=%s' % str(user_id)) # We support Python 2.3, but the @-deco syntax would look like # this: # @tools.login_redir() def secure(self): return 'Welcome!' secure = tools.login_redir()(secure) # Since calling the tool returns the same function you pass in, # you could skip binding the return value, and just write: # tools.login_redir()(secure) def login(self): return 'Please log in' def custom_err(self): return 'Something went horribly wrong.' @cherrypy.config(**{'hooks.before_request_body': redir_custom}) def early_ir(self, arg): return 'whatever' class Image(Test): def getImagesByUser(self, user_id): return '0 images for %s' % user_id class Flatten(Test): def as_string(self): return 'content' def as_list(self): return ['con', 'tent'] def as_yield(self): yield b'content' @cherrypy.config(**{'tools.flatten.on': True}) def as_dblyield(self): yield self.as_yield() def as_refyield(self): for chunk in self.as_yield(): yield chunk class Ranges(Test): def get_ranges(self, bytes): return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) def slice_file(self): path = os.path.join(os.getcwd(), os.path.dirname(__file__)) return static.serve_file( os.path.join(path, 'static/index.html')) class Cookies(Test): def single(self, name): cookie = cherrypy.request.cookie[name] # Python2's SimpleCookie.__setitem__ won't take unicode keys. cherrypy.response.cookie[str(name)] = cookie.value def multiple(self, names): list(map(self.single, names)) def append_headers(header_list, debug=False): if debug: cherrypy.log( 'Extending response headers with %s' % repr(header_list), 'TOOLS.APPEND_HEADERS') cherrypy.serving.response.header_list.extend(header_list) cherrypy.tools.append_headers = cherrypy.Tool( 'on_end_resource', append_headers) class MultiHeader(Test): def header_list(self): pass header_list = cherrypy.tools.append_headers(header_list=[ (b'WWW-Authenticate', b'Negotiate'), (b'WWW-Authenticate', b'Basic realm="foo"'), ])(header_list) def commas(self): cherrypy.response.headers[ 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' cherrypy.tree.mount(root) def testStatus(self): self.getPage('/status/') self.assertBody('normal') self.assertStatus(200) self.getPage('/status/blank') self.assertBody('') self.assertStatus(200) self.getPage('/status/illegal') self.assertStatus(500) msg = 'Illegal response status from server (781 is out of range).' self.assertErrorPage(500, msg) if not getattr(cherrypy.server, 'using_apache', False): self.getPage('/status/unknown') self.assertBody('funky') self.assertStatus(431) self.getPage('/status/bad') self.assertStatus(500) msg = "Illegal response status from server ('error' is non-numeric)." self.assertErrorPage(500, msg) def test_on_end_resource_status(self): self.getPage('/status/on_end_resource_stage') self.assertBody('[]') self.getPage('/status/on_end_resource_stage') self.assertBody(repr(['200 OK'])) def testSlashes(self): # Test that requests for index methods without a trailing slash # get redirected to the same URI path with a trailing slash. # Make sure GET params are preserved. self.getPage('/redirect?id=3') self.assertStatus(301) self.assertMatchesBody( '' '%s/redirect/[?]id=3' % (self.base(), self.base()) ) if self.prefix(): # Corner case: the "trailing slash" redirect could be tricky if # we're using a virtual root and the URI is "/vroot" (no slash). self.getPage('') self.assertStatus(301) self.assertMatchesBody("%s/" % (self.base(), self.base())) # Test that requests for NON-index methods WITH a trailing slash # get redirected to the same URI path WITHOUT a trailing slash. # Make sure GET params are preserved. self.getPage('/redirect/by_code/?code=307') self.assertStatus(301) self.assertMatchesBody( "" '%s/redirect/by_code[?]code=307' % (self.base(), self.base()) ) # If the trailing_slash tool is off, CP should just continue # as if the slashes were correct. But it needs some help # inside cherrypy.url to form correct output. self.getPage('/url?path_info=page1') self.assertBody('%s/url/page1' % self.base()) self.getPage('/url/leaf/?path_info=page1') self.assertBody('%s/url/page1' % self.base()) def testRedirect(self): self.getPage('/redirect/') self.assertBody('child') self.assertStatus(200) self.getPage('/redirect/by_code?code=300') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(300) self.getPage('/redirect/by_code?code=301') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(301) self.getPage('/redirect/by_code?code=302') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(302) self.getPage('/redirect/by_code?code=303') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(303) self.getPage('/redirect/by_code?code=307') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(307) self.getPage('/redirect/by_code?code=308') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(308) self.getPage('/redirect/nomodify') self.assertBody('') self.assertStatus(304) self.getPage('/redirect/proxy') self.assertBody('') self.assertStatus(305) # HTTPRedirect on error self.getPage('/redirect/error/') self.assertStatus(('302 Found', '303 See Other')) self.assertInBody('/errpage') # Make sure str(HTTPRedirect()) works. self.getPage('/redirect/stringify', protocol='HTTP/1.0') self.assertStatus(200) self.assertBody("(['%s/'], 302)" % self.base()) if cherrypy.server.protocol_version == 'HTTP/1.1': self.getPage('/redirect/stringify', protocol='HTTP/1.1') self.assertStatus(200) self.assertBody("(['%s/'], 303)" % self.base()) # check that #fragments are handled properly # http://skrb.org/ietf/http_errata.html#location-fragments frag = 'foo' self.getPage('/redirect/fragment/%s' % frag) self.assertMatchesBody( r"\2\/some\/url\#%s" % ( frag, frag)) loc = self.assertHeader('Location') assert loc.endswith('#%s' % frag) self.assertStatus(('302 Found', '303 See Other')) # check injection protection # See https://github.com/cherrypy/cherrypy/issues/1003 self.getPage( '/redirect/custom?' 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') self.assertStatus(303) loc = self.assertHeader('Location') assert 'Set-Cookie' in loc self.assertNoHeader('Set-Cookie') def assertValidXHTML(): from xml.etree import ElementTree try: ElementTree.fromstring( '%s' % self.body, ) except ElementTree.ParseError: self._handlewebError( 'automatically generated redirect did not ' 'generate well-formed html', ) # check redirects to URLs generated valid HTML - we check this # by seeing if it appears as valid XHTML. self.getPage('/redirect/by_code?code=303') self.assertStatus(303) assertValidXHTML() # do the same with a url containing quote characters. self.getPage('/redirect/url_with_quote') self.assertStatus(303) assertValidXHTML() def test_redirect_with_xss(self): """A redirect to a URL with HTML injected should result in page contents escaped.""" self.getPage('/redirect/url_with_xss') self.assertStatus(303) assert b' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/advanced.rst0000644252176402575230000005535614307416152017500 0ustar00jaracoprimarygroup.. _advanced: Advanced -------- CherryPy has support for more advanced features that these sections will describe. .. contents:: :depth: 4 .. _aliases: Set aliases to page handlers ############################ A fairly unknown, yet useful, feature provided by the :func:`cherrypy.expose` decorator is to support aliases. Let's use the template provided by :ref:`tutorial 03 `: .. code-block:: python import random import string import cherrypy class StringGenerator(object): @cherrypy.expose(['generer', 'generar']) def generate(self, length=8): return ''.join(random.sample(string.hexdigits, int(length))) if __name__ == '__main__': cherrypy.quickstart(StringGenerator()) In this example, we create localized aliases for the page handler. This means the page handler will be accessible via: - /generate - /generer (French) - /generar (Spanish) Obviously, your aliases may be whatever suits your needs. .. note:: The alias may be a single string or a list of them. .. _restful: RESTful-style dispatching ######################### The term `RESTful URL` is sometimes used to talk about friendly URLs that nicely map to the entities an application exposes. .. important:: We will not enter the debate around what is restful or not but we will showcase two mechanisms to implement the usual idea in your CherryPy application. Let's assume you wish to create an application that exposes music bands and their records. Your application will probably have the following URLs: - http://hostname// - http://hostname//albums// It's quite clear you would not create a page handler named after every possible band in the world. This means you will need a page handler that acts as a proxy for all of them. The default dispatcher cannot deal with that scenario on its own because it expects page handlers to be explicitly declared in your source code. Luckily, CherryPy provides ways to support those use cases. .. seealso:: This section extends from this `stackoverflow response `_. The special _cp_dispatch method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``_cp_dispatch`` is a special method you declare in any of your :term:`controller` to massage the remaining segments before CherryPy gets to process them. This offers you the capacity to remove, add or otherwise handle any segment you wish and, even, entirely change the remaining parts. .. code-block:: python import cherrypy class Band(object): def __init__(self): self.albums = Album() def _cp_dispatch(self, vpath): if len(vpath) == 1: cherrypy.request.params['name'] = vpath.pop() return self if len(vpath) == 3: cherrypy.request.params['artist'] = vpath.pop(0) # /band name/ vpath.pop(0) # /albums/ cherrypy.request.params['title'] = vpath.pop(0) # /album title/ return self.albums return vpath @cherrypy.expose def index(self, name): return 'About %s...' % name class Album(object): @cherrypy.expose def index(self, artist, title): return 'About %s by %s...' % (title, artist) if __name__ == '__main__': cherrypy.quickstart(Band()) Notice how the controller defines `_cp_dispatch`, it takes a single argument, the URL path info broken into its segments. The method can inspect and manipulate the list of segments, removing any or adding new segments at any position. The new list of segments is then sent to the dispatcher which will use it to locate the appropriate resource. In the above example, you should be able to go to the following URLs: - http://localhost:8080/nirvana/ - http://localhost:8080/nirvana/albums/nevermind/ The ``/nirvana/`` segment is associated to the band and the ``/nevermind/`` segment relates to the album. To achieve this, our `_cp_dispatch` method works on the idea that the default dispatcher matches URLs against page handler signatures and their position in the tree of handlers. In this case, we take the dynamic segments in the URL (band and record names), we inject them into the request parameters and we remove them from the segment lists as if they had never been there in the first place. In other words, `_cp_dispatch` makes it as if we were working on the following URLs: - http://localhost:8080/?artist=nirvana - http://localhost:8080/albums/?artist=nirvana&title=nevermind The popargs decorator ^^^^^^^^^^^^^^^^^^^^^ :func:`cherrypy.popargs` is more straightforward as it gives a name to any segment that CherryPy wouldn't be able to interpret otherwise. This makes the matching of segments with page handler signatures easier and helps CherryPy understand the structure of your URL. .. code-block:: python import cherrypy @cherrypy.popargs('band_name') class Band(object): def __init__(self): self.albums = Album() @cherrypy.expose def index(self, band_name): return 'About %s...' % band_name @cherrypy.popargs('album_title') class Album(object): @cherrypy.expose def index(self, band_name, album_title): return 'About %s by %s...' % (album_title, band_name) if __name__ == '__main__': cherrypy.quickstart(Band()) This works similarly to `_cp_dispatch` but, as said above, is more explicit and localized. It says: - take the first segment and store it into a parameter named `band_name` - take again the first segment (since we removed the previous first) and store it into a parameter named `album_title` Note that the decorator accepts more than a single binding. For instance: .. code-block:: python @cherrypy.popargs('album_title') class Album(object): def __init__(self): self.tracks = Track() @cherrypy.popargs('track_num', 'track_title') class Track(object): @cherrypy.expose def index(self, band_name, album_title, track_num, track_title): ... This would handle the following URL: - http://localhost:8080/nirvana/albums/nevermind/tracks/06/polly Notice finally how the whole stack of segments is passed to each page handler so that you have the full context. Error handling ############## CherryPy's ``HTTPError`` class supports raising immediate responses in the case of errors. .. code-block:: python class Root: @cherrypy.expose def thing(self, path): if not authorized(): raise cherrypy.HTTPError(401, 'Unauthorized') try: file = open(path) except FileNotFoundError: raise cherrypy.HTTPError(404) ``HTTPError.handle`` is a context manager which supports translating exceptions raised in the app into an appropriate HTTP response, as in the second example. .. code-block:: python class Root: @cherrypy.expose def thing(self, path): with cherrypy.HTTPError.handle(FileNotFoundError, 404): file = open(path) Streaming the response body ########################### CherryPy handles HTTP requests, packing and unpacking the low-level details, then passing control to your application's :term:`page handler`, which produce the body of the response. CherryPy allows you to return body content in a variety of types: a string, a list of strings, a file. CherryPy also allows you to *yield* content, rather than *return* content. When you use "yield", you also have the option of streaming the output. **In general, it is safer and easier to not stream output.** Therefore, streaming output is off by default. Streaming output and also using sessions requires a good understanding of :py:mod:`how session locks work `. The "normal" CherryPy response process ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When you provide content from your page handler, CherryPy manages the conversation between the HTTP server and your code like this: .. image:: _static/images/cpreturn.gif Notice that the HTTP server gathers all output first and then writes everything to the client at once: status, headers, and body. This works well for static or simple pages, since the entire response can be changed at any time, either in your application code, or by the CherryPy framework. How "streaming output" works with CherryPy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When you set the config entry "response.stream" to True (and use "yield"), CherryPy manages the conversation between the HTTP server and your code like this: .. image:: _static/images/cpyield.gif When you stream, your application doesn't immediately pass raw body content back to CherryPy or to the HTTP server. Instead, it passes back a generator. At that point, CherryPy finalizes the status and headers, **before** the generator has been consumed, or has produced any output. This is necessary to allow the HTTP server to send the headers and pieces of the body as they become available. Once CherryPy has set the status and headers, it sends them to the HTTP server, which then writes them out to the client. From that point on, the CherryPy framework mostly steps out of the way, and the HTTP server essentially requests content directly from your application code (your page handler method). Therefore, when streaming, if an error occurs within your page handler, CherryPy will not catch it--the HTTP server will catch it. Because the headers (and potentially some of the body) have already been written to the client, the server *cannot* know a safe means of handling the error, and will therefore simply close the connection (the current, builtin servers actually write out a short error message in the body, but this may be changed, and is not guaranteed behavior for all HTTP servers you might use with CherryPy). In addition, you cannot manually modify the status or headers within your page handler if that handler method is a streaming generator, because the method will not be iterated over until after the headers have been written to the client. **This includes raising exceptions like HTTPError, NotFound, InternalRedirect and HTTPRedirect.** To use a streaming generator while modifying headers, you would have to return a generator that is separate from (or embedded in) your page handler. For example: .. code-block:: python class Root: @cherrypy.expose def thing(self): cherrypy.response.headers['Content-Type'] = 'text/plain' if not authorized(): raise cherrypy.NotFound() def content(): yield "Hello, " yield "world" return content() thing._cp_config = {'response.stream': True} Streaming generators are sexy, but they play havoc with HTTP. CherryPy allows you to stream output for specific situations: pages which take many minutes to produce, or pages which need a portion of their content immediately output to the client. Because of the issues outlined above, **it is usually better to flatten (buffer) content rather than stream content**. Do otherwise only when the benefits of streaming outweigh the risks. Response timing ############### CherryPy responses include an attribute: * ``response.time``: the :func:`time.time` at which the response began Deal with signals ################# This :ref:`engine plugin ` is instantiated automatically as `cherrypy.engine.signal_handler`. However, it is only *subscribed* automatically by :func:`cherrypy.quickstart`. So if you want signal handling and you're calling: .. code-block:: python tree.mount() engine.start() engine.block() on your own, be sure to add before you start the engine: .. code-block:: python engine.signals.subscribe() .. index:: Windows, Ctrl-C, shutdown .. _windows-console: Windows Console Events ^^^^^^^^^^^^^^^^^^^^^^ Microsoft Windows uses console events to communicate some signals, like Ctrl-C. Deploying CherryPy on Windows platforms requires `Python for Windows Extensions `_, which are installed automatically, being provided an extra dependency with environment marker. With that installed, CherryPy will handle Ctrl-C and other console events (CTRL_C_EVENT, CTRL_LOGOFF_EVENT, CTRL_BREAK_EVENT, CTRL_SHUTDOWN_EVENT, and CTRL_CLOSE_EVENT) automatically, shutting down the bus in preparation for process exit. Securing your server #################### .. note:: This section is not meant as a complete guide to securing a web application or ecosystem. Please review the various guides provided at `OWASP `_. There are several settings that can be enabled to make CherryPy pages more secure. These include: Transmitting data: #. Use Secure Cookies Rendering pages: #. Set HttpOnly cookies #. Set XFrame options #. Enable XSS Protection #. Set the Content Security Policy An easy way to accomplish this is to set headers with a tool and wrap your entire CherryPy application with it: .. code-block:: python import cherrypy # set the priority according to your needs if you are hooking something # else on the 'before_finalize' hook point. @cherrypy.tools.register('before_finalize', priority=60) def secureheaders(): headers = cherrypy.response.headers headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' headers['Content-Security-Policy'] = "default-src 'self';" .. note:: Read more about `those headers `_. Then, in the :ref:`configuration file ` (or any other place that you want to enable the tool): .. code-block:: ini [/] tools.secureheaders.on = True If you use :ref:`sessions ` you can also enable these settings: .. code-block:: ini [/] tools.sessions.on = True # increase security on sessions tools.sessions.secure = True tools.sessions.httponly = True If you use SSL you can also enable Strict Transport Security: .. code-block:: python # add this to secureheaders(): # only add Strict-Transport headers if we're actually using SSL; see the ietf spec # "An HSTS Host MUST NOT include the STS header field in HTTP responses # conveyed over non-secure transport" # http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14#section-7.2 if (cherrypy.server.ssl_certificate != None and cherrypy.server.ssl_private_key != None): headers['Strict-Transport-Security'] = 'max-age=31536000' # one year Next, you should probably use :ref:`SSL `. Multiple HTTP servers support ############################# CherryPy starts its own HTTP server whenever you start the engine. In some cases, you may wish to host your application on more than a single port. This is easily achieved: .. code-block:: python from cherrypy._cpserver import Server server = Server() server.socket_port = 8090 server.subscribe() You can create as many :class:`server ` server instances as you need, once :ref:`subscribed `, they will follow the CherryPy engine's life-cycle. WSGI support ############ CherryPy supports the WSGI interface defined in :pep:`333` as well as its updates in :pep:`3333`. It means the following: - You can host a foreign WSGI application with the CherryPy server - A CherryPy application can be hosted by another WSGI server Make your CherryPy application a WSGI application ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A WSGI application can be obtained from your application as follows: .. code-block:: python import cherrypy wsgiapp = cherrypy.Application(StringGenerator(), '/', config=myconf) Simply use the `wsgiapp` instance in any WSGI-aware server. .. _hostwsgiapp: Host a foreign WSGI application in CherryPy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Assuming you have a WSGI-aware application, you can host it in your CherryPy server using the :meth:`cherrypy.tree.graft ` facility. .. code-block:: python def raw_wsgi_app(environ, start_response): status = '200 OK' response_headers = [('Content-type','text/plain')] start_response(status, response_headers) return ['Hello world!'] cherrypy.tree.graft(raw_wsgi_app, '/') .. important:: You cannot use tools with a foreign WSGI application. However, you can still benefit from the :ref:`CherryPy bus `. No need for the WSGI interface? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The default CherryPy HTTP server supports the WSGI interfaces defined in :pep:`333` and :pep:`3333`. However, if your application is a pure CherryPy application, you can switch to a HTTP server that by-passes the WSGI layer altogether. It will provide a slight performance increase. .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def index(self): return "Hello World!" if __name__ == '__main__': from cherrypy._cpnative_server import CPHTTPServer cherrypy.server.httpserver = CPHTTPServer(cherrypy.server) cherrypy.quickstart(Root(), '/') .. important:: Using the native server, you will not be able to graft a WSGI application as shown in the previous section. Doing so will result in a server error at runtime. WebSocket support ################# `WebSocket `_ is a recent application protocol that came to life from the HTML5 working-group in response to the needs for bi-directional communication. Various hacks had been proposed such as Comet, polling, etc. WebSocket is a socket that starts its life from a HTTP upgrade request. Once the upgrade is performed, the underlying socket is kept opened but not used in a HTTP context any longer. Instead, both connected endpoints may use the socket to push data to the other end. CherryPy itself does not support WebSocket, but the feature is provided by an external library called `ws4py `_. Database support ################ CherryPy does not bundle any database access but its architecture makes it easy to integrate common database interfaces such as the DB-API specified in :pep:`249`. Alternatively, you can also use an `ORM `_ such as `SQLAlchemy `_ or `SQLObject `_. You will find a recipe at `cherrypy-recipes `_ that explains how to integrate SQLAlchemy using a mix of :ref:`plugins ` and :ref:`tools `. HTML Templating support ####################### CherryPy does not provide any HTML template but its architecture makes it easy to integrate one. Popular ones are `Mako `_ or `Jinja2 `_. You will find `here `_ a recipe on how to integrate them using a mix :ref:`plugins ` and :ref:`tools `. Testing your application ######################## Web applications, like any other kind of code, must be tested. CherryPy provides a :class:`helper class ` to ease writing functional tests. Here is a simple example for a basic echo application: .. code-block:: python import cherrypy from cherrypy.test import helper class SimpleCPTest(helper.CPWebCase): def setup_server(): class Root(object): @cherrypy.expose def echo(self, message): return message cherrypy.tree.mount(Root()) setup_server = staticmethod(setup_server) def test_message_should_be_returned_as_is(self): self.getPage("/echo?message=Hello%20world") self.assertStatus('200 OK') self.assertHeader('Content-Type', 'text/html;charset=utf-8') self.assertBody('Hello world') def test_non_utf8_message_will_fail(self): """ CherryPy defaults to decode the query-string using UTF-8, trying to send a query-string with a different encoding will raise a 404 since it considers it's a different URL. """ self.getPage("/echo?message=A+bient%F4t", headers=[ ('Accept-Charset', 'ISO-8859-1,utf-8'), ('Content-Type', 'text/html;charset=ISO-8859-1') ] ) self.assertStatus('404 Not Found') As you can see the, test inherits from that helper class. You should setup your application and mount it as per-usual. Then, define your various tests and call the helper :meth:`~cherrypy.test.helper.CPWebCase.getPage` method to perform a request. Simply use the various specialized assert* methods to validate your workflow and data. You can then run the test using `py.test `_ as follows: .. code-block:: bash $ py.test -s test_echo_app.py The ``-s`` is necessary because the CherryPy class also wraps stdin and stdout. Without the flag, tests may hang on failed assertions waiting for an input. Another option to avoid this problem, (if, for example, you are running tests inside an IDE) is to disable the interactive mode that's enabled by default. It can be disabled setting the ``WEBTEST_INTERACTIVE`` environment variable to ``False`` or ``0``. If you don't want to change environment variables to simply run a suite of tests you could also subclass the :class:`helper class `, set ``helper.CPWebCase.interactive = False`` in the class and then derive all your test classes from your custom class: .. code-block:: python import cherrypy from cherrypy.test import helper class TestsBase(helper.CPWebCase): helper.CPWebCase.interactive = False .. note:: Although they are written using the typical pattern the :mod:`unittest` module supports, they are not bare unit tests. Indeed, a whole CherryPy stack is started for you and runs your application. If you want to really unit test your CherryPy application, meaning without having to start a server, you may want to have a look at this `recipe `_. .. note:: The :class:`helper class ` derives from :class:`unittest.TestCase` class. For this reason, running from `pytest `_, there are some limitations with respect to standard ``pytest`` tests, especially if you are grouping the tests in test classes. You can find more details at `this page `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/basics.rst0000644252176402575230000006071714307416152017174 0ustar00jaracoprimarygroup.. _basics: Basics ------ The following sections will drive you through the basics of a CherryPy application, introducing some essential concepts. .. contents:: :depth: 4 The one-minute application example ################################## The most basic application you can write with CherryPy involves almost all its core concepts. .. code-block:: python :linenos: import cherrypy class Root(object): @cherrypy.expose def index(self): return "Hello World!" if __name__ == '__main__': cherrypy.quickstart(Root(), '/') First and foremost, for most tasks, you will never need more than a single import statement as demonstrated in line 1. Before discussing the meat, let's jump to line 9 which shows, how to host your application with the CherryPy application server and serve it with its builtin HTTP server at the `'/'` path. All in one single line. Not bad. Let's now step back to the actual application. Even though CherryPy does not mandate it, most of the time your applications will be written as Python classes. Methods of those classes will be called by CherryPy to respond to client requests. However, CherryPy needs to be aware that a method can be used that way, we say the method needs to be :term:`exposed`. This is precisely what the :func:`cherrypy.expose()` decorator does in line 4. Save the snippet in a file named `myapp.py` and run your first CherryPy application: .. code-block:: bash $ python myapp.py Then point your browser at http://127.0.0.1:8080. Tada! .. note:: CherryPy is a small framework that focuses on one single task: take a HTTP request and locate the most appropriate Python function or method that match the request's URL. Unlike other well-known frameworks, CherryPy does not provide a built-in support for database access, HTML templating or any other middleware nifty features. In a nutshell, once CherryPy has found and called an :term:`exposed` method, it is up to you, as a developer, to provide the tools to implement your application's logic. CherryPy takes the opinion that you, the developer, know best. .. warning:: The previous example demonstrated the simplicty of the CherryPy interface but, your application will likely contain a few other bits and pieces: static service, more complex structure, database access, etc. This will be developed in the tutorial section. CherryPy is a minimal framework but not a bare one, it comes with a few basic tools to cover common usages that you would expect. Hosting one or more applications ################################ A web application needs an HTTP server to be accessed to. CherryPy provides its own, production ready, HTTP server. There are two ways to host an application with it. The simple one and the almost-as-simple one. Single application ^^^^^^^^^^^^^^^^^^ The most straightforward way is to use :func:`cherrypy.quickstart` function. It takes at least one argument, the instance of the application to host. Two other settings are optionals. First, the base path at which the application will be accessible from. Second, a config dictionary or file to configure your application. .. code-block:: python cherrypy.quickstart(Blog()) cherrypy.quickstart(Blog(), '/blog') cherrypy.quickstart(Blog(), '/blog', {'/': {'tools.gzip.on': True}}) The first one means that your application will be available at http://hostname:port/ whereas the other two will make your blog application available at http://hostname:port/blog. In addition, the last one provides specific settings for the application. .. note:: Notice in the third case how the settings are still relative to the application, not where it is made available at, hence the `{'/': ... }` rather than a `{'/blog': ... }` Multiple applications ^^^^^^^^^^^^^^^^^^^^^ The :func:`cherrypy.quickstart` approach is fine for a single application, but lacks the capacity to host several applications with the server. To achieve this, one must use the :meth:`cherrypy.tree.mount ` function as follows: .. code-block:: python cherrypy.tree.mount(Blog(), '/blog', blog_conf) cherrypy.tree.mount(Forum(), '/forum', forum_conf) cherrypy.engine.start() cherrypy.engine.block() Essentially, :meth:`cherrypy.tree.mount ` takes the same parameters as :func:`cherrypy.quickstart`: an :term:`application`, a hosting path segment and a configuration. The last two lines are simply starting application server. .. important:: :func:`cherrypy.quickstart` and :meth:`cherrypy.tree.mount ` are not exclusive. For instance, the previous lines can be written as: .. code-block:: python cherrypy.tree.mount(Blog(), '/blog', blog_conf) cherrypy.quickstart(Forum(), '/forum', forum_conf) .. note:: You can also :ref:`host foreign WSGI application `. Logging ####### Logging is an important task in any application. CherryPy will log all incoming requests as well as protocol errors. To do so, CherryPy manages two loggers: - an access one that logs every incoming requests - an application/error log that traces errors or other application-level messages Your application may leverage that second logger by calling :func:`cherrypy.log() `. .. code-block:: python cherrypy.log("hello there") You can also log an exception: .. code-block:: python try: ... except Exception: cherrypy.log("kaboom!", traceback=True) Both logs are writing to files identified by the following keys in your configuration: - ``log.access_file`` for incoming requests using the `common log format `_ - ``log.error_file`` for the other log .. seealso:: Refer to the :mod:`cherrypy._cplogging` module for more details about CherryPy's logging architecture. Disable logging ^^^^^^^^^^^^^^^ You may be interested in disabling either logs. To disable file logging, simply set a en empty string to the ``log.access_file`` or ``log.error_file`` keys in your :ref:`global configuration `. To disable, console logging, set ``log.screen`` to `False`. .. code-block:: python cherrypy.config.update({'log.screen': False, 'log.access_file': '', 'log.error_file': ''}) Play along with your other loggers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Your application may obviously already use the :mod:`logging` module to trace application level messages. Below is a simple example on setting it up. .. code-block:: python import logging import logging.config import cherrypy logger = logging.getLogger() db_logger = logging.getLogger('db') LOG_CONF = { 'version': 1, 'formatters': { 'void': { 'format': '' }, 'standard': { 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' }, }, 'handlers': { 'default': { 'level':'INFO', 'class':'logging.StreamHandler', 'formatter': 'standard', 'stream': 'ext://sys.stdout' }, 'cherrypy_console': { 'level':'INFO', 'class':'logging.StreamHandler', 'formatter': 'void', 'stream': 'ext://sys.stdout' }, 'cherrypy_access': { 'level':'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'void', 'filename': 'access.log', 'maxBytes': 10485760, 'backupCount': 20, 'encoding': 'utf8' }, 'cherrypy_error': { 'level':'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'void', 'filename': 'errors.log', 'maxBytes': 10485760, 'backupCount': 20, 'encoding': 'utf8' }, }, 'loggers': { '': { 'handlers': ['default'], 'level': 'INFO' }, 'db': { 'handlers': ['default'], 'level': 'INFO' , 'propagate': False }, 'cherrypy.access': { 'handlers': ['cherrypy_access'], 'level': 'INFO', 'propagate': False }, 'cherrypy.error': { 'handlers': ['cherrypy_console', 'cherrypy_error'], 'level': 'INFO', 'propagate': False }, } } class Root(object): @cherrypy.expose def index(self): logger.info("boom") db_logger.info("bam") cherrypy.log("bang") return "hello world" if __name__ == '__main__': cherrypy.config.update({'log.screen': False, 'log.access_file': '', 'log.error_file': ''}) cherrypy.engine.unsubscribe('graceful', cherrypy.log.reopen_files) logging.config.dictConfig(LOG_CONF) cherrypy.quickstart(Root()) In this snippet, we create a `configuration dictionary `_ that we pass on to the ``logging`` module to configure our loggers: * the default root logger is associated to a single stream handler * a logger for the db backend with also a single stream handler In addition, we re-configure the CherryPy loggers: * the top-level ``cherrypy.access`` logger to log requests into a file * the ``cherrypy.error`` logger to log everything else into a file and to the console We also prevent CherryPy from trying to open its log files when the autoreloader kicks in. This is not strictly required since we do not even let CherryPy open them in the first place. But, this avoids wasting time on something useless. .. _config: Configuring ########### CherryPy comes with a fine-grained configuration mechanism and settings can be set at various levels. .. seealso:: Once you have the reviewed the basics, please refer to the :ref:`in-depth discussion ` around configuration. .. _globalsettings: Global server configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^ To configure the HTTP and application servers, use the :meth:`cherrypy.config.update() ` method. .. code-block:: python cherrypy.config.update({'server.socket_port': 9090}) The :mod:`cherrypy.config ` object is a dictionary and the update method merges the passed dictionary into it. You can also pass a file instead (assuming a `server.conf` file): .. code-block:: ini [global] server.socket_port: 9090 .. code-block:: python cherrypy.config.update("server.conf") .. warning:: :meth:`cherrypy.config.update() ` is not meant to be used to configure the application. It is a common mistake. It is used to configure the server and engine. .. _perappconf: Per-application configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To configure your application, pass in a dictionary or a file when you associate your application to the server. .. code-block:: python cherrypy.quickstart(myapp, '/', {'/': {'tools.gzip.on': True}}) or via a file (called `app.conf` for instance): .. code-block:: ini [/] tools.gzip.on: True .. code-block:: python cherrypy.quickstart(myapp, '/', "app.conf") Although, you can define most of your configuration in a global fashion, it is sometimes convenient to define them where they are applied in the code. .. code-block:: python class Root(object): @cherrypy.expose @cherrypy.tools.gzip() def index(self): return "hello world!" A variant notation to the above: .. code-block:: python class Root(object): @cherrypy.expose def index(self): return "hello world!" index._cp_config = {'tools.gzip.on': True} Both methods have the same effect so pick the one that suits your style best. Additional application settings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can add settings that are not specific to a request URL and retrieve them from your page handler as follows: .. code-block:: ini [/] tools.gzip.on: True [googleapi] key = "..." appid = "..." .. code-block:: python class Root(object): @cherrypy.expose def index(self): google_appid = cherrypy.request.app.config['googleapi']['appid'] return "hello world!" cherrypy.quickstart(Root(), '/', "app.conf") Cookies ####### CherryPy uses the :mod:`Cookie` module from python and in particular the :class:`Cookie.SimpleCookie` object type to handle cookies. - To send a cookie to a browser, set ``cherrypy.response.cookie[key] = value``. - To retrieve a cookie sent by a browser, use ``cherrypy.request.cookie[key]``. - To delete a cookie (on the client side), you must *send* the cookie with its expiration time set to `0`: .. code-block:: python cherrypy.response.cookie[key] = value cherrypy.response.cookie[key]['expires'] = 0 It's important to understand that the request cookies are **not** automatically copied to the response cookies. Clients will send the same cookies on every request, and therefore ``cherrypy.request.cookie`` should be populated each time. But the server doesn't need to send the same cookies with every response; therefore, ``cherrypy.response.cookie`` will usually be empty. When you wish to “delete” (expire) a cookie, therefore, you must set ``cherrypy.response.cookie[key] = value`` first, and then set its ``expires`` attribute to 0. Extended example: .. code-block:: python import cherrypy class MyCookieApp(object): @cherrypy.expose def set(self): cookie = cherrypy.response.cookie cookie['cookieName'] = 'cookieValue' cookie['cookieName']['path'] = '/' cookie['cookieName']['max-age'] = 3600 cookie['cookieName']['version'] = 1 return "Hello, I just sent you a cookie" @cherrypy.expose def read(self): cookie = cherrypy.request.cookie res = """Hi, you sent me %s cookies.
Here is a list of cookie names/values:
""" % len(cookie) for name in cookie.keys(): res += "name: %s, value: %s
" % (name, cookie[name].value) return res + "" if __name__ == '__main__': cherrypy.quickstart(MyCookieApp(), '/cookie') .. _basicsession: Using sessions ############## Sessions are one of the most common mechanism used by developers to identify users and synchronize their activity. By default, CherryPy does not activate sessions because it is not a mandatory feature to have, to enable it simply add the following settings in your configuration: .. code-block:: ini [/] tools.sessions.on: True .. code-block:: python cherrypy.quickstart(myapp, '/', "app.conf") Sessions are, by default, stored in RAM so, if you restart your server all of your current sessions will be lost. You can store them in memcached or on the filesystem instead. Using sessions in your applications is done as follows: .. code-block:: python import cherrypy @cherrypy.expose def index(self): if 'count' not in cherrypy.session: cherrypy.session['count'] = 0 cherrypy.session['count'] += 1 In this snippet, everytime the index page handler is called, the current user's session has its `'count'` key incremented by `1`. CherryPy knows which session to use by inspecting the cookie sent alongside the request. This cookie contains the session identifier used by CherryPy to load the user's session from the storage. .. seealso:: Refer to the :mod:`cherrypy.lib.sessions` module for more details about the session interface and implementation. Notably you will learn about sessions expiration. Filesystem backend ^^^^^^^^^^^^^^^^^^ Using a filesystem is a simple to not lose your sessions between reboots. Each session is saved in its own file within the given directory. .. code-block:: ini [/] tools.sessions.on: True tools.sessions.storage_class = cherrypy.lib.sessions.FileSession tools.sessions.storage_path = "/some/directory" Memcached backend ^^^^^^^^^^^^^^^^^ `Memcached `_ is a popular key-store on top of your RAM, it is distributed and a good choice if you want to share sessions outside of the process running CherryPy. Requires that the Python `memcached package `_ is installed, which may be indicated by installing ``cherrypy[memcached_session]``. .. code-block:: ini [/] tools.sessions.on: True tools.sessions.storage_class = cherrypy.lib.sessions.MemcachedSession .. _staticontent: Other backends ^^^^^^^^^^^^^^ Any other library may implement a session backend. Simply subclass ``cherrypy.lib.sessions.Session`` and indicate that subclass as ``tools.sessions.storage_class``. Static content serving ###################### CherryPy can serve your static content such as images, javascript and CSS resources, etc. .. note:: CherryPy uses the :mod:`mimetypes` module to determine the best content-type to serve a particular resource. If the choice is not valid, you can simply set more media-types as follows: .. code-block:: python import mimetypes mimetypes.types_map['.csv'] = 'text/csv' Serving a single file ^^^^^^^^^^^^^^^^^^^^^ You can serve a single file as follows: .. code-block:: ini [/style.css] tools.staticfile.on = True tools.staticfile.filename = "/home/site/style.css" CherryPy will automatically respond to URLs such as `http://hostname/style.css`. Serving a whole directory ^^^^^^^^^^^^^^^^^^^^^^^^^ Serving a whole directory is similar to a single file: .. code-block:: ini [/static] tools.staticdir.on = True tools.staticdir.dir = "/home/site/static" Assuming you have a file at `static/js/my.js`, CherryPy will automatically respond to URLs such as `http://hostname/static/js/my.js`. .. note:: CherryPy always requires the absolute path to the files or directories it will serve. If you have several static sections to configure but located in the same root directory, you can use the following shortcut: .. code-block:: ini [/] tools.staticdir.root = "/home/site" [/static] tools.staticdir.on = True tools.staticdir.dir = "static" Specifying an index file ^^^^^^^^^^^^^^^^^^^^^^^^^ By default, CherryPy will respond to the root of a static directory with an 404 error indicating the path '/' was not found. To specify an index file, you can use the following: .. code-block:: ini [/static] tools.staticdir.on = True tools.staticdir.dir = "/home/site/static" tools.staticdir.index = "index.html" Assuming you have a file at `static/index.html`, CherryPy will automatically respond to URLs such as `http://hostname/static/` by returning its contents. Allow files downloading ^^^^^^^^^^^^^^^^^^^^^^^ Using ``"application/x-download"`` response content-type, you can tell a browser that a resource should be downloaded onto the user's machine rather than displayed. You could for instance write a page handler as follows: .. code-block:: python from cherrypy.lib.static import serve_file @cherrypy.expose def download(self, filepath): return serve_file(filepath, "application/x-download", "attachment") Assuming the filepath is a valid path on your machine, the response would be considered as a downloadable content by the browser. .. warning:: The above page handler is a security risk on its own since any file of the server could be accessed (if the user running the server had permissions on them). Dealing with JSON ################# CherryPy has built-in support for JSON encoding and decoding of the request and/or response. Decoding request ^^^^^^^^^^^^^^^^ To automatically decode the content of a request using JSON: .. code-block:: python class Root(object): @cherrypy.expose @cherrypy.tools.json_in() def index(self): data = cherrypy.request.json The `json` attribute attached to the request contains the decoded content. Encoding response ^^^^^^^^^^^^^^^^^ To automatically encode the content of a response using JSON: .. code-block:: python class Root(object): @cherrypy.expose @cherrypy.tools.json_out() def index(self): return {'key': 'value'} CherryPy will encode any content returned by your page handler using JSON. Not all type of objects may natively be encoded. Authentication ############## CherryPy provides support for two very simple HTTP-based authentication mechanisms, described in :rfc:`7616` and :rfc:`7617` (which obsoletes :rfc:`2617`): Basic and Digest. They are most commonly known to trigger a browser's popup asking users their name and password. Basic ^^^^^ Basic authentication is the simplest form of authentication however it is not a secure one as the user's credentials are embedded into the request. We advise against using it unless you are running on SSL or within a closed network. .. code-block:: python from cherrypy.lib import auth_basic USERS = {'jon': 'secret'} def validate_password(realm, username, password): if username in USERS and USERS[username] == password: return True return False conf = { '/protected/area': { 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'localhost', 'tools.auth_basic.checkpassword': validate_password, 'tools.auth_basic.accept_charset': 'UTF-8', } } cherrypy.quickstart(myapp, '/', conf) Simply put, you have to provide a function that will be called by CherryPy passing the username and password decoded from the request. The function can read its data from any source it has to: a file, a database, memory, etc. Digest ^^^^^^ Digest authentication differs by the fact the credentials are not carried on by the request so it's a little more secure than basic. CherryPy's digest support has a similar interface to the basic one explained above. .. code-block:: python from cherrypy.lib import auth_digest USERS = {'jon': 'secret'} conf = { '/protected/area': { 'tools.auth_digest.on': True, 'tools.auth_digest.realm': 'localhost', 'tools.auth_digest.get_ha1': auth_digest.get_ha1_dict_plain(USERS), 'tools.auth_digest.key': 'a565c27146791cfb', 'tools.auth_digest.accept_charset': 'UTF-8', } } cherrypy.quickstart(myapp, '/', conf) SO_PEERCRED ^^^^^^^^^^^ There's also a low-level authentication for UNIX file and abstract sockets. This is how you enable it: .. code-block:: ini [global] server.peercreds: True server.peercreds_resolve: True server.socket_file: /var/run/cherrypy.sock ``server.peercreds`` enables looking up the connected process ID, user ID and group ID. They'll be accessible as WSGI environment variables: * ``X_REMOTE_PID`` * ``X_REMOTE_UID`` * ``X_REMOTE_GID`` ``server.peercreds_resolve`` resolves that into user name and group name. They'll be accessible as WSGI environment variables: * ``X_REMOTE_USER`` and ``REMOTE_USER`` * ``X_REMOTE_GROUP`` Favicon ####### CherryPy serves its own sweet red cherrypy as the default `favicon `_ using the static file tool. You can serve your own favicon as follows: .. code-block:: python import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello World!" if __name__ == '__main__': cherrypy.quickstart(HelloWorld(), '/', { '/favicon.ico': { 'tools.staticfile.on': True, 'tools.staticfile.filename': '/path/to/myfavicon.ico' } } ) Please refer to the :ref:`static serving ` section for more details. You can also use a file to configure it: .. code-block:: ini [/favicon.ico] tools.staticfile.on: True tools.staticfile.filename: "/path/to/myfavicon.ico" .. code-block:: python import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello World!" if __name__ == '__main__': cherrypy.quickstart(HelloWorld(), '/', "app.conf") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/docs/conf.py0000644252176402575230000001635614424762003016474 0ustar00jaracoprimarygroup"""CherryPy sphinx doc configuration module.""" # -*- coding: utf-8 -*- # # CherryPy documentation build configuration file, created by # sphinx-quickstart on Sat Feb 20 09:18:03 2010. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from email import message_from_string import importlib import pkg_resources import sys assert sys.version_info > (3, 5), 'Python 3 required to build docs' def try_import(mod_name): """Attempt importing module and suppress failure of doing this.""" try: return importlib.import_module(mod_name) except ImportError: pass def get_supported_pythons(classifiers): """Return min and max supported Python version from meta as tuples.""" PY_VER_CLASSIFIER = 'Programming Language :: Python :: ' vers = filter(lambda c: c.startswith(PY_VER_CLASSIFIER), classifiers) vers = map(lambda c: c[len(PY_VER_CLASSIFIER):], vers) vers = filter(lambda c: c[0].isdigit() and '.' in c, vers) vers = map(lambda c: tuple(map(int, c.split('.'))), vers) vers = sorted(vers) del vers[1:-1] return vers custom_sphinx_theme = try_import('alabaster') prj_dist = pkg_resources.get_distribution('cherrypy') prj_pkg_info = prj_dist.get_metadata(prj_dist.PKG_INFO) prj_meta = message_from_string(prj_pkg_info) prj_author = prj_meta['Author'] prj_license = prj_meta['License'] prj_description = prj_meta['Description'] prj_py_ver_range = get_supported_pythons(prj_meta.get_all('Classifier')) prj_py_min_supported, prj_py_max_supported = map( lambda v: '.'.join(map(str, v)), prj_py_ver_range ) project = prj_dist.project_name github_url = 'https://github.com' github_repo_org = project.lower() github_repo_name = project.lower() github_repo_slug = f'{github_repo_org}/{github_repo_name}' github_repo_url = f'{github_url}/{github_repo_slug}' cr_github_repo_url = f'{github_url}/{github_repo_org}/cheroot' github_sponsors_url = f'{github_url}/sponsors' rst_epilog = f""" .. |project| replace:: {project} .. |min_py_supported| replace:: {prj_py_min_supported} .. |max_py_supported| replace:: {prj_py_max_supported} """ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ # Stdlib extensions: 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', # Third-party extensions: 'sphinxcontrib.apidoc', 'rst.linker', 'jaraco.packaging.sphinx', ] extlinks = { 'issue': (f'{github_repo_url}/issues/%s', '#%s'), 'pr': (f'{github_repo_url}/pull/%s', 'PR #%s'), 'commit': (f'{github_repo_url}/commit/%s', '%s'), 'cr-issue': (f'{cr_github_repo_url}/issues/%s', 'Cheroot #%s'), 'cr-pr': (f'{cr_github_repo_url}/pull/%s', 'Cheroot PR #%s'), 'gh': (f'{github_url}/%s', 'GitHub: %s'), 'user': (f'{github_sponsors_url}/%s', '@%s'), } intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'cheroot': ('https://cheroot.cherrypy.dev/en/latest/', None), 'pytest-docs': ('https://docs.pytest.org/en/latest/', None), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output --------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = getattr(custom_sphinx_theme, '__name__', 'default') # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { # "relbarbgcolor": "#880000", # "relbartextcolor": "white", # "relbarlinkcolor": "#FFEEEE", # "sidebarbgcolor": "#880000", # "sidebartextcolor": "white", # "sidebarlinkcolor": "#FFEEEE", # "headbgcolor": "#FFF8FB", # "headtextcolor": "black", # "headlinkcolor": "#660000", # "footerbgcolor": "#880000", # "footertextcolor": "white", # "codebgcolor": "#FFEEEE", # } html_theme_options = { 'logo': 'images/cherrypy_logo_big.png', 'github_user': project.lower(), 'github_repo': project.lower(), 'github_button': True, 'github_banner': True, 'github_type': 'star', 'github_count': True, 'travis_button': True, 'codecov_button': True, # 'analytics_id': ..., } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # html_style = 'cpdocmain.css' # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': [ 'about.html', 'searchbox.html', 'navigation.html', 'python_2_eol.html', ], '**': [ 'about.html', 'searchbox.html', 'navigation.html', 'python_2_eol.html', ], } # Output file base name for HTML help builder. htmlhelp_basename = 'CherryPydoc' # -- Options for LaTeX output -------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # documentclass [howto/manual]). latex_documents = [ ( 'index', 'CherryPy.tex', 'CherryPy Documentation', 'CherryPy Team', 'manual', ), ] link_files = { '../CHANGES.rst': dict( using=dict( GH='https://github.com', ), replace=[ dict( pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), ], ), } # Ref: https://github.com/python-attrs/attrs/pull/571/files\ # #diff-85987f48f1258d9ee486e3191495582dR82 default_role = 'any' # -- Options for autodoc extension --------------------------------------- autodoc_mock_imports = [ 'win32api', 'win32con', 'win32event', 'win32service', 'win32serviceutil', ] # -- Options for apidoc extension ---------------------------------------- apidoc_excluded_paths = [] apidoc_extra_args = [ '--implicit-namespaces', '--private', # include “_private” modules ] apidoc_module_dir = '../cherrypy' apidoc_module_first = False apidoc_output_dir = 'pkg' apidoc_separate_modules = True apidoc_toc_file = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/config.rst0000644252176402575230000004405014307416152017165 0ustar00jaracoprimarygroup.. _configindepth: Configure --------- Configuration in CherryPy is implemented via dictionaries. Keys are strings which name the mapped value; values may be of any type. In CherryPy 3, you use configuration (files or dicts) to set attributes directly on the engine, server, request, response, and log objects. So the best way to know the full range of what's available in the config file is to simply import those objects and see what ``help(obj)`` tells you. .. note:: If you are new to CherryPy, please refer first to the simpler :ref:`basic config ` section first. .. contents:: :depth: 3 Architecture ############ The first thing you need to know about CherryPy 3's configuration is that it separates *global* config from *application* config. If you're deploying multiple *applications* at the same *site* (and more and more people are, as Python web apps are tending to decentralize), you need to be careful to separate the configurations, as well. There's only ever one "global config", but there is a separate "app config" for each app you deploy. CherryPy *Requests* are part of an *Application*, which runs in a *global* context, and configuration data may apply to any of those three scopes. Let's look at each of those scopes in turn. Global config ^^^^^^^^^^^^^ Global config entries apply everywhere, and are stored in :class:`cherrypy.config `. This flat dict only holds global config data; that is, "site-wide" config entries which affect all mounted applications. Global config is stored in the :class:`cherrypy.config ` dict, and you therefore update it by calling ``cherrypy.config.update(conf)``. The ``conf`` argument can be either a filename, an open file, or a dict of config entries. Here's an example of passing a dict argument: .. code-block:: python cherrypy.config.update({'server.socket_host': '64.72.221.48', 'server.socket_port': 80, }) The ``server.socket_host`` option in this example determines on which network interface CherryPy will listen. The ``server.socket_port`` option declares the TCP port on which to listen. Application config ^^^^^^^^^^^^^^^^^^ Application entries apply to a single mounted application, and are stored on each Application object itself as :attr:`app.config `. This is a two-level dict where each top-level key is a path, or "relative URL" (for example, ``"/"`` or ``"/my/page"``), and each value is a dict of config entries. The URL's are relative to the script name (mount point) of the Application. Usually, all this data is provided in the call to ``tree.mount(root(), script_name='/path/to', config=conf)``, although you may also use ``app.merge(conf)``. The ``conf`` argument can be either a filename, an open file, or a dict of config entries. Configuration file example: .. code-block:: ini [/] tools.trailing_slash.on = False request.dispatch: cherrypy.dispatch.MethodDispatcher() or, in python code: .. code-block:: python config = {'/': { 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), 'tools.trailing_slash.on': False, } } cherrypy.tree.mount(Root(), config=config) CherryPy only uses sections that start with ``"/"`` (except ``[global]``, see below). That means you can place your own configuration entries in a CherryPy config file by giving them a section name which does not start with ``"/"``. For example, you might include database entries like this: .. code-block:: ini [global] server.socket_host: "0.0.0.0" [Databases] driver: "postgres" host: "localhost" port: 5432 [/path] response.timeout: 6000 Then, in your application code you can read these values during request time via ``cherrypy.request.app.config['Databases']``. For code that is outside the request process, you'll have to pass a reference to your Application around. Request config ^^^^^^^^^^^^^^ Each Request object possesses a single :attr:`request.config ` dict. Early in the request process, this dict is populated by merging Global config, Application config, and any config acquired while looking up the page handler (see next). This dict contains only those config entries which apply to the given request. .. note:: when you do an :class:`InternalRedirect`, this config attribute is recalculated for the new path. Declaration ########### Configuration data may be supplied as a Python dictionary, as a filename, or as an open file object. Configuration files ^^^^^^^^^^^^^^^^^^^ When you supply a filename or file, CherryPy uses Python's builtin ConfigParser; you declare Application config by writing each path as a section header, and each entry as a ``"key: value"`` (or ``"key = value"``) pair: .. code-block:: ini [/path/to/my/page] response.stream: True tools.trailing_slash.extra = False Combined Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are only deploying a single application, you can make a single config file that contains both global and app entries. Just stick the global entries into a config section named ``[global]``, and pass the same file to both :func:`config.update ` and :func:`tree.mount `, and application config is usually passed in a call to :func:`cherrypy.tree.mount `. In general, you should set global config first, and then mount each application with its own config. Among other benefits, this allows you to set up global logging so that, if something goes wrong while trying to mount an application, you'll see the tracebacks. In other words, use this order: .. code-block:: python # global config cherrypy.config.update({'environment': 'production', 'log.error_file': 'site.log', # ... }) # Mount each app and pass it its own config cherrypy.tree.mount(root1, "", appconf1) cherrypy.tree.mount(root2, "/forum", appconf2) cherrypy.tree.mount(root3, "/blog", appconf3) if hasattr(cherrypy.engine, 'block'): # 3.1 syntax cherrypy.engine.start() cherrypy.engine.block() else: # 3.0 syntax cherrypy.server.quickstart() cherrypy.engine.start() Values in config files use Python syntax ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Config entries are always a key/value pair, like ``server.socket_port = 8080``. The key is always a name, and the value is always a Python object. That is, if the value you are setting is an ``int`` (or other number), it needs to look like a Python ``int``; for example, ``8080``. If the value is a string, it needs to be quoted, just like a Python string. Arbitrary objects can also be created, just like in Python code (assuming they can be found/imported). Here's an extended example, showing you some of the different types: .. code-block:: ini [global] log.error_file: "/home/fumanchu/myapp.log" environment = 'production' server.max_request_body_size: 1200 [/myapp] tools.trailing_slash.on = False request.dispatch: cherrypy.dispatch.MethodDispatcher() .. _cp_config: _cp_config: attaching config to handlers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Config files have a severe limitation: values are always keyed by URL. For example: .. code-block:: ini [/path/to/page] methods_with_bodies = ("POST", "PUT", "PROPPATCH") It's obvious that the extra method is the norm for that path; in fact, the code could be considered broken without it. In CherryPy, you can attach that bit of config directly on the page handler: .. code-block:: python @cherrypy.expose def page(self): return "Hello, world!" page._cp_config = {"request.methods_with_bodies": ("POST", "PUT", "PROPPATCH")} ``_cp_config`` is a reserved attribute which the dispatcher looks for at each node in the object tree. The ``_cp_config`` attribute must be a CherryPy config dictionary. If the dispatcher finds a ``_cp_config`` attribute, it merges that dictionary into the rest of the config. The entire merged config dictionary is placed in :attr:`cherrypy.request.config `. This can be done at any point in the tree of objects; for example, we could have attached that config to a class which contains the page method: .. code-block:: python class SetOPages: _cp_config = {"request.methods_with_bodies": ("POST", "PUT", "PROPPATCH")} @cherrypy.expose def page(self): return "Hullo, Werld!" .. note:: This behavior is only guaranteed for the default dispatcher. Other dispatchers may have different restrictions on where you can attach ``_cp_config`` attributes. Additionally, because the dispatcher is is responsible for processing ``_cp_config``, it is not possible to change the dispatcher (i.e. ``request.dispatch`` is not honored at this construct). This technique allows you to: * Put config near where it's used for improved readability and maintainability. * Attach config to objects instead of URL's. This allows multiple URL's to point to the same object, yet you only need to define the config once. * Provide defaults which are still overridable in a config file. .. _namespaces: Namespaces ########## Because config entries usually just set attributes on objects, they're almost all of the form: ``object.attribute``. A few are of the form: ``object.subobject.attribute``. They look like normal Python attribute chains, because they work like them. We call the first name in the chain the *"config namespace"*. When you provide a config entry, it is bound as early as possible to the actual object referenced by the namespace; for example, the entry ``response.stream`` actually sets the ``stream`` attribute of :class:`cherrypy.response `! In this way, you can easily determine the default value by firing up a python interpreter and typing: .. code-block:: python >>> import cherrypy >>> cherrypy.response.stream False Each config namespace has its own handler; for example, the "request" namespace has a handler which takes your config entry and sets that value on the appropriate "request" attribute. There are a few namespaces, however, which don't work like normal attributes behind the scenes; however, they still use dotted keys and are considered to "have a namespace". Builtin namespaces ^^^^^^^^^^^^^^^^^^ Entries from each namespace may be allowed in the global, application root (``"/"``) or per-path config, or a combination: ========== ====== ================== ========= Scope Global Application Root App Path ---------- ------ ------------------ --------- engine X hooks X X X log X X request X X X response X X X server X tools X X X ========== ====== ================== ========= engine ~~~~~~ Entries in this namespace controls the 'application engine'. These can only be declared in the global config. Any attribute of :class:`cherrypy.engine` may be set in config; however, there are a few extra entries available in config: * Plugin attributes. Many of the :ref:`Engine Plugins` are themselves attributes of ``cherrypy.engine``. You can set any attribute of an attached plugin by simply naming it. For example, there is an instance of the :class:`Autoreloader` class at ``engine.autoreload``; you can set its "frequency" attribute via the config entry ``engine.autoreload.frequency = 60``. In addition, you can turn such plugins on and off by setting ``engine.autoreload.on = True`` or ``False``. * ``engine.SIGHUP/SIGTERM``: These entries can be used to set the list of listeners for the given :ref:`channel`. Mostly, this is used to turn off the signal handling one gets automatically via :func:`cherrypy.quickstart`. hooks ~~~~~ Declares additional request-processing functions. Use this to append your own :class:`Hook` functions to the request. For example, to add ``my_hook_func`` to the ``before_handler`` hookpoint: .. code-block:: ini [/] hooks.before_handler = myapp.my_hook_func log ~~~ Configures logging. These can only be declared in the global config (for global logging) or ``[/]`` config (for each application). See :class:`LogManager` for the list of configurable attributes. Typically, the "access_file", "error_file", and "screen" attributes are the most commonly configured. request ~~~~~~~ Sets attributes on each Request. See the :class:`Request` class for a complete list. response ~~~~~~~~ Sets attributes on each Response. See the :class:`Response` class for a complete list. server ~~~~~~ Controls the default HTTP server via :class:`cherrypy.server` (see that class for a complete list of configurable attributes). These can only be declared in the global config. tools ~~~~~ Enables and configures additional request-processing packages. See the :doc:`/tutorial/tools` overview for more information. wsgi ~~~~ Adds WSGI middleware to an Application's "pipeline". These can only be declared in the app's root config ("/"). * ``wsgi.pipeline``: Appends to the WSGi pipeline. The value must be a list of (name, app factory) pairs. Each app factory must be a WSGI callable class (or callable that returns a WSGI callable); it must take an initial 'nextapp' argument, plus any optional keyword arguments. The optional arguments may be configured via ``wsgi..``. * ``wsgi.response_class``: Overrides the default :class:`Response` class. checker ~~~~~~~ Controls the "checker", which looks for common errors in app state (including config) when the engine starts. You can turn off individual checks by setting them to ``False`` in config. See :class:`cherrypy._cpchecker.Checker` for a complete list. Global config only. Custom config namespaces ^^^^^^^^^^^^^^^^^^^^^^^^ You can define your own namespaces if you like, and they can do far more than simply set attributes. The ``test/test_config`` module, for example, shows an example of a custom namespace that coerces incoming params and outgoing body content. The :mod:`cherrypy._cpwsgi` module includes an additional, builtin namespace for invoking WSGI middleware. In essence, a config namespace handler is just a function, that gets passed any config entries in its namespace. You add it to a namespaces registry (a dict), where keys are namespace names and values are handler functions. When a config entry for your namespace is encountered, the corresponding handler function will be called, passing the config key and value; that is, ``namespaces[namespace](k, v)``. For example, if you write: .. code-block:: python def db_namespace(k, v): if k == 'connstring': orm.connect(v) cherrypy.config.namespaces['db'] = db_namespace then ``cherrypy.config.update({"db.connstring": "Oracle:host=1.10.100.200;sid=TEST"})`` will call ``db_namespace('connstring', 'Oracle:host=1.10.100.200;sid=TEST')``. The point at which your namespace handler is called depends on where you add it: =========== ============================================================================= =================================== Scope Namespace dict Handler is called in ----------- ----------------------------------------------------------------------------- ----------------------------------- Global :attr:`cherrypy.config.namespaces ` cherrypy.config.update Application :attr:`app.namespaces ` Application.merge (which is called by cherrypy.tree.mount) Request :attr:`app.request_class.namespaces ` Request.configure (called for each request, after the handler is looked up) =========== ============================================================================= =================================== The name can be any string, and the handler must be either a callable or a (Python 2.5 style) context manager. If you need additional code to run when all your namespace keys are collected, you can supply a callable context manager in place of a normal function for the handler. Context managers are defined in :pep:`343`. .. _environments: Environments ^^^^^^^^^^^^ The only key that does not exist in a namespace is the *"environment"* entry. It only applies to the global config, and only when you use :func:`cherrypy.config.update `. This special entry *imports* other config entries from the following template stored in ``cherrypy._cpconfig.environments[environment]``. .. literalinclude:: ../cherrypy/_cpconfig.py :start-after: Sphinx begin config.environments :end-before: Sphinx end config.environments If you find the set of existing environments (production, staging, etc) too limiting or just plain wrong, feel free to extend them or add new environments: .. code-block:: python cherrypy._cpconfig.environments['staging']['log.screen'] = False cherrypy._cpconfig.environments['Greek'] = { 'tools.encode.encoding': 'ISO-8859-7', 'tools.decode.encoding': 'ISO-8859-7', } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/contribute.rst0000644252176402575230000000406114307416152020074 0ustar00jaracoprimarygroupContribute ---------- CherryPy is a community-maintained, open-source project hosted at Github. The project actively encourages aspiring and experienced users to dive in and add their best contribution to the project. How can you contribute? Well, first search the `docs `_ and the `project page `_ to see if someone has already reported your issue. StackOverflow ============= On `StackOverflow `_, there are questions tagged with 'cherrypy'. Answer unanswered questions, add an improved answer, clarify an answer with a comment, or ask more meaningful questions there. Earn reputation and share experience. Filing Bug Reports ================== If you find a bug, an issue where the product doesn't behave as you expect, you may file a bug report at `the project page `_. Be sure to include what your expectation was, what happened instead, details about your system that might be relevant, and steps that someone else could take to replicate your finding. The more detailed and exact your description, the better one of the volunteers on the project may be able to help resolve your issue. Fixing Bugs =========== CherryPy has a number of open, reported `issues `_. Some of them are complicated and difficult, but others are more straightforward and shovel-ready. Feel free to find one that you think you can solve or introduce yourself and ask for guidance in `our gitter channel `_. As you work through the issue and commit changes to your clone of the repository, be sure to add issue references to your changes (like "Fixes #999" or "Ref #999") so your changes link to the issue and vice-versa. Writing Pull Requests ===================== To contribute, first read `How to write the perfect pull request `_ and file your contribution with the `CherryPy Project page `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/deploy.rst0000644252176402575230000004456214307416152017224 0ustar00jaracoprimarygroup Deploy ------ CherryPy stands on its own, but as an application server, it is often located in shared or complex environments. For this reason, it is not uncommon to run CherryPy behind a reverse proxy or use other servers to host the application. .. note:: CherryPy's server has proven reliable and fast enough for years now. If the volume of traffic you receive is average, it will do well enough on its own. Nonetheless, it is common to delegate the serving of static content to more capable servers such as `nginx `_ or CDN. .. contents:: :depth: 3 Run as a daemon ############### CherryPy allows you to easily decouple the current process from the parent environment, using the traditional double-fork: .. code-block:: python from cherrypy.process.plugins import Daemonizer d = Daemonizer(cherrypy.engine) d.subscribe() .. note:: This :ref:`engine plugin ` is only available on Unix and similar systems which provide `fork()`. If a startup error occurs in the forked children, the return code from the parent process will still be 0. Errors in the initial daemonizing process still return proper exit codes, but errors after the fork won't. Therefore, if you use this plugin to daemonize, don't use the return code as an accurate indicator of whether the process fully started. In fact, that return code only indicates if the process successfully finished the first fork. The plugin takes optional arguments to redirect standard streams: ``stdin``, ``stdout``, and ``stderr``. By default, these are all redirected to :file:`/dev/null`, but you're free to send them to log files or elsewhere. .. warning:: You should be careful to not start any threads before this plugin runs. The plugin will warn if you do so, because "...the effects of calling functions that require certain resources between the call to fork() and the call to an exec function are undefined". (`ref `_). It is for this reason that the Server plugin runs at priority 75 (it starts worker threads), which is later than the default priority of 65 for the Daemonizer. Run as a different user ####################### Use this :ref:`engine plugin ` to start your CherryPy site as root (for example, to listen on a privileged port like 80) and then reduce privileges to something more restricted. This priority of this plugin's "start" listener is slightly higher than the priority for `server.start` in order to facilitate the most common use: starting on a low port (which requires root) and then dropping to another user. .. code-block:: python DropPrivileges(cherrypy.engine, uid=1000, gid=1000).subscribe() PID files ######### The PIDFile :ref:`engine plugin ` is pretty straightforward: it writes the process id to a file on start, and deletes the file on exit. You must provide a 'pidfile' argument, preferably an absolute path: .. code-block:: python PIDFile(cherrypy.engine, '/var/run/myapp.pid').subscribe() Systemd socket activation ######################### Socket Activation is a systemd feature that allows to setup a system so that the systemd will sit on a port and start services 'on demand' (a little bit like inetd and xinetd used to do). CherryPy has built-in socket activation support, if run from a systemd service file it will detect the `LISTEN_PID` environment variable to know that it should consider fd 3 to be the passed socket. To read more about socket activation: http://0pointer.de/blog/projects/socket-activation.html Control via Supervisord ####################### `Supervisord `_ is a powerful process control and management tool that can perform a lot of tasks around process monitoring. Below is a simple supervisor configuration for your CherryPy application. .. code-block:: ini [unix_http_server] file=/tmp/supervisor.sock [supervisord] logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log) logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) logfile_backups=10 ; (num of main logfile rotation backups;default 10) loglevel=info ; (log level;default info; others: debug,warn,trace) pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) nodaemon=false ; (start in foreground if true;default false) minfds=1024 ; (min. avail startup file descriptors;default 1024) minprocs=200 ; (min. avail process descriptors;default 200) [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///tmp/supervisor.sock [program:myapp] command=python server.py environment=PYTHONPATH=. directory=. This could control your server via the ``server.py`` module as the application entry point. .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def index(self): return "Hello World!" cherrypy.config.update({'server.socket_port': 8090, 'engine.autoreload.on': False, 'log.access_file': './access.log', 'log.error_file': './error.log'}) cherrypy.quickstart(Root()) To take the configuration (assuming it was saved in a file called ``supervisor.conf``) into account: .. code-block:: bash $ supervisord -c supervisord.conf $ supervisorctl update Now, you can point your browser at http://localhost:8090/ and it will display `Hello World!`. To stop supervisor, type: .. code-block:: bash $ supervisorctl shutdown This will obviously shutdown your application. .. _ssl: SSL support ########### .. note:: You may want to test your server for SSL using the services from `Qualys, Inc. `_ CherryPy can encrypt connections using SSL to create an https connection. This keeps your web traffic secure. Here's how. 1. Generate a private key. We'll use openssl and follow the `OpenSSL Keys HOWTO `_.: .. code-block:: bash $ openssl genrsa -out privkey.pem 2048 You can create either a key that requires a password to use, or one without a password. Protecting your private key with a password is much more secure, but requires that you enter the password every time you use the key. For example, you may have to enter the password when you start or restart your CherryPy server. This may or may not be feasible, depending on your setup. If you want to require a password, add one of the ``-aes128``, ``-aes192`` or ``-aes256`` switches to the command above. You should not use any of the DES, 3DES, or SEED algorithms to protect your password, as they are insecure. SSL Labs recommends using 2048-bit RSA keys for security (see references section at the end). 2. Generate a certificate. We'll use openssl and follow the `OpenSSL Certificates HOWTO `_. Let's start off with a self-signed certificate for testing: .. code-block:: bash $ openssl req -new -x509 -days 365 -key privkey.pem -out cert.pem openssl will then ask you a series of questions. You can enter whatever values are applicable, or leave most fields blank. The one field you *must* fill in is the 'Common Name': enter the hostname you will use to access your site. If you are just creating a certificate to test on your own machine and you access the server by typing 'localhost' into your browser, enter the Common Name 'localhost'. 3. Decide whether you want to use python's built-in SSL library, or the pyOpenSSL library. CherryPy supports either. a) *Built-in.* To use python's built-in SSL, add the following line to your CherryPy config: .. code-block:: python cherrypy.server.ssl_module = 'builtin' b) *pyOpenSSL*. Because python did not have a built-in SSL library when CherryPy was first created, the default setting is to use pyOpenSSL. To use it you'll need to install it (we could recommend you install `cython `_ first): .. code-block:: bash $ pip install cython, pyOpenSSL 4. Add the following lines in your CherryPy config to point to your certificate files: .. code-block:: python cherrypy.server.ssl_certificate = "cert.pem" cherrypy.server.ssl_private_key = "privkey.pem" 5. If you have a certificate chain at hand, you can also specify it: .. code-block:: python cherrypy.server.ssl_certificate_chain = "certchain.perm" 6. Start your CherryPy server normally. Note that if you are debugging locally and/or using a self-signed certificate, your browser may show you security warnings. WSGI servers ############ Embedding into another WSGI framework ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Though CherryPy comes with a very reliable and fast enough HTTP server, you may wish to integrate your CherryPy application within a different framework. To do so, we will benefit from the WSGI interface defined in :pep:`333` and :pep:`3333`. Note that you should follow some basic rules when embedding CherryPy in a third-party WSGI server: - If you rely on the `"main"` channel to be published on, as it would happen within the CherryPy's mainloop, you should find a way to publish to it within the other framework's mainloop. - Start the CherryPy's engine. This will publish to the `"start"` channel of the bus. .. code-block:: python cherrypy.engine.start() - Stop the CherryPy's engine when you terminate. This will publish to the `"stop"` channel of the bus. .. code-block:: python cherrypy.engine.stop() - Do not call ``cherrypy.engine.block()``. - Disable the built-in HTTP server since it will not be used. .. code-block:: python cherrypy.server.unsubscribe() - Disable autoreload. Usually other frameworks won't react well to it, or sometimes, provide the same feature. .. code-block:: python cherrypy.config.update({'engine.autoreload.on': False}) - Disable CherryPy signals handling. This may not be needed, it depends on how the other framework handles them. .. code-block:: python cherrypy.engine.signals.subscribe() - Use the ``"embedded"`` environment configuration scheme. .. code-block:: python cherrypy.config.update({'environment': 'embedded'}) Essentially this will disable the following: - Stdout logging - Autoreloader - Configuration checker - Headers logging on error - Tracebacks in error - Mismatched params error during dispatching - Signals (SIGHUP, SIGTERM) Tornado ^^^^^^^ You can use `tornado `_ HTTP server as follow: .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def index(self): return "Hello World!" if __name__ == '__main__': import tornado import tornado.httpserver import tornado.wsgi # our WSGI application wsgiapp = cherrypy.tree.mount(Root()) # Disable the autoreload which won't play well cherrypy.config.update({'engine.autoreload.on': False}) # let's not start the CherryPy HTTP server cherrypy.server.unsubscribe() # use CherryPy's signal handling cherrypy.engine.signals.subscribe() # Prevent CherryPy logs to be propagated # to the Tornado logger cherrypy.log.error_log.propagate = False # Run the engine but don't block on it cherrypy.engine.start() # Run thr tornado stack container = tornado.wsgi.WSGIContainer(wsgiapp) http_server = tornado.httpserver.HTTPServer(container) http_server.listen(8080) # Publish to the CherryPy engine as if # we were using its mainloop tornado.ioloop.PeriodicCallback(lambda: cherrypy.engine.publish('main'), 100).start() tornado.ioloop.IOLoop.instance().start() Twisted ^^^^^^^ You can use `Twisted `_ HTTP server as follow: .. code-block:: python import cherrypy from twisted.web.wsgi import WSGIResource from twisted.internet import reactor from twisted.internet import task # Our CherryPy application class Root(object): @cherrypy.expose def index(self): return "hello world" # Create our WSGI app from the CherryPy application wsgiapp = cherrypy.tree.mount(Root()) # Configure the CherryPy's app server # Disable the autoreload which won't play well cherrypy.config.update({'engine.autoreload.on': False}) # We will be using Twisted HTTP server so let's # disable the CherryPy's HTTP server entirely cherrypy.server.unsubscribe() # If you'd rather use CherryPy's signal handler # Uncomment the next line. I don't know how well this # will play with Twisted however #cherrypy.engine.signals.subscribe() # Publish periodically onto the 'main' channel as the bus mainloop would do task.LoopingCall(lambda: cherrypy.engine.publish('main')).start(0.1) # Tie our app to Twisted reactor.addSystemEventTrigger('after', 'startup', cherrypy.engine.start) reactor.addSystemEventTrigger('before', 'shutdown', cherrypy.engine.exit) resource = WSGIResource(reactor, reactor.getThreadPool(), wsgiapp) Notice how we attach the bus methods to the Twisted's own lifecycle. Save that code into a module named `cptw.py` and run it as follows: .. code-block:: bash $ twistd -n web --port 8080 --wsgi cptw.wsgiapp uwsgi ^^^^^ You can use `uwsgi `_ HTTP server as follow: .. code-block:: python import cherrypy # Our CherryPy application class Root(object): @cherrypy.expose def index(self): return "hello world" cherrypy.config.update({'engine.autoreload.on': False}) cherrypy.server.unsubscribe() cherrypy.engine.start() wsgiapp = cherrypy.tree.mount(Root()) Save this into a Python module called `mymod.py` and run it as follows: .. code-block:: bash $ uwsgi --socket 127.0.0.1:8080 --protocol=http --wsgi-file mymod.py --callable wsgiapp Virtual Hosting ############### CherryPy has support for virtual-hosting. It does so through a dispatchers that locate the appropriate resource based on the requested domain. Below is a simple example for it: .. code-block:: python import cherrypy class Root(object): def __init__(self): self.app1 = App1() self.app2 = App2() class App1(object): @cherrypy.expose def index(self): return "Hello world from app1" class App2(object): @cherrypy.expose def index(self): return "Hello world from app2" if __name__ == '__main__': hostmap = { 'company.com:8080': '/app1', 'home.net:8080': '/app2', } config = { 'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap) } cherrypy.quickstart(Root(), '/', {'/': config}) In this example, we declare two domains and their ports: - company.com:8080 - home.net:8080 Thanks to the :class:`cherrypy.dispatch.VirtualHost` dispatcher, we tell CherryPy which application to dispatch to when a request arrives. The dispatcher looks up the requested domain and call the according application. .. note:: To test this example, simply add the following rules to your `hosts` file: .. code-block:: text 127.0.0.1 company.com 127.0.0.1 home.net Reverse-proxying ################ Apache ^^^^^^ Nginx ^^^^^ nginx is a fast and modern HTTP server with a small footprint. It is a popular choice as a reverse proxy to application servers such as CherryPy. This section will not cover the whole range of features nginx provides. Instead, it will simply provide you with a basic configuration that can be a good starting point. .. code-block:: nginx :linenos: upstream apps { server 127.0.0.1:8080; server 127.0.0.1:8081; } gzip_http_version 1.0; gzip_proxied any; gzip_min_length 500; gzip_disable "MSIE [1-6]\."; gzip_types text/plain text/xml text/css text/javascript application/javascript; server { listen 80; server_name www.example.com; access_log /app/logs/www.example.com.log combined; error_log /app/logs/www.example.com.log; location ^~ /static/ { root /app/static/; } location / { proxy_pass http://apps; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; } } Edit this configuration to match your own paths. Then, save this configuration into a file under ``/etc/nginx/conf.d/`` (assuming Ubuntu). The filename is irrelevant. Then run the following commands: .. code-block:: bash $ sudo service nginx stop $ sudo service nginx start Hopefully, this will be enough to forward requests hitting the nginx frontend to your CherryPy application. The ``upstream`` block defines the addresses of your CherryPy instances. It shows that you can load-balance between two application servers. Refer to the nginx documentation to understand how this achieved. .. code-block:: nginx upstream apps { server 127.0.0.1:8080; server 127.0.0.1:8081; } Later on, this block is used to define the reverse proxy section. Now, let's see our application: .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def index(self): return "hello world" if __name__ == '__main__': cherrypy.config.update({ 'server.socket_port': 8080, 'tools.proxy.on': True, 'tools.proxy.base': 'http://www.example.com' }) cherrypy.quickstart(Root()) If you run two instances of this code, one on each port defined in the nginx section, you will be able to reach both of them via the load-balancing done by nginx. Notice how we define the proxy tool. It is not mandatory and used only so that the CherryPy request knows about the true client's address. Otherwise, it would know only about the nginx's own address. This is most visible in the logs. The ``base`` attribute should match the ``server_name`` section of the nginx configuration. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/development.rst0000644252176402575230000000035714307416152020244 0ustar00jaracoprimarygroupTesting ------- - To run the regression tests, first install tox: .. code:: sh pip install 'tox>=2.5' then run it .. code:: sh tox - To run individual tests type: .. code:: sh tox -- -k test_foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/enterprise.rst0000644252176402575230000000103714307416152020076 0ustar00jaracoprimarygroup.. todo: consider linking to the same text in README.rst. .. https://github.com/cherrypy/cherrypy/pull/1813#discussion_r330805322 For Enterprise ============== CherryPy is available as part of the Tidelift Subscription. The CherryPy maintainers and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/extend.rst0000644252176402575230000005267714307416152017225 0ustar00jaracoprimarygroup.. _extend: Extend ------ CherryPy is truly an open framework, you can extend and plug new functions at will either server-side or on a per-requests basis. Either way, CherryPy is made to help you build your application and support your architecture via simple patterns. .. contents:: :depth: 4 Server-wide functions ##################### CherryPy can be considered both as a HTTP library as much as a web application framework. In that latter case, its architecture provides mechanisms to support operations across the whole server instance. This offers a powerful canvas to perform persistent operations as server-wide functions live outside the request processing itself. They are available to the whole process as long as the bus lives. Typical use cases: - Keeping a pool of connection to an external server so that your need not to re-open them on each request (database connections for instance). - Background processing (say you need work to be done without blocking the whole request itself). Publish/Subscribe pattern ^^^^^^^^^^^^^^^^^^^^^^^^^ CherryPy's backbone consists of a bus system implementing a simple `publish/subscribe messaging pattern `_. Simply put, in CherryPy everything is controlled via that bus. One can easily picture the bus as a sushi restaurant's belt as in the picture below. .. image:: _static/images/sushibelt.JPG :target: http://en.wikipedia.org/wiki/YO!_Sushi You can subscribe and publish to channels on a bus. A channel is bit like a unique identifier within the bus. When a message is published to a channel, the bus will dispatch the message to all subscribers for that channel. One interesting aspect of a pubsub pattern is that it promotes decoupling between a caller and the callee. A published message will eventually generate a response but the publisher does not know where that response came from. Thanks to that decoupling, a CherryPy application can easily access functionalities without having to hold a reference to the entity providing that functionality. Instead, the application simply publishes onto the bus and will receive the appropriate response, which is all that matter. .. _buspattern: Typical pattern ~~~~~~~~~~~~~~~ Let's take the following dummy application: .. code-block:: python import cherrypy class ECommerce(object): def __init__(self, db): self.mydb = db @cherrypy.expose def save_kart(self, cart_data): cart = Cart(cart_data) self.mydb.save(cart) if __name__ == '__main__': cherrypy.quickstart(ECommerce(), '/') The application has a reference to the database but this creates a fairly strong coupling between the database provider and the application. Another approach to work around the coupling is by using a pubsub workflow: .. code-block:: python import cherrypy class ECommerce(object): @cherrypy.expose def save_kart(self, cart_data): cart = Cart(cart_data) cherrypy.engine.publish('db-save', cart) if __name__ == '__main__': cherrypy.quickstart(ECommerce(), '/') In this example, we publish a `cart` instance to `db-save` channel. One or many subscribers can then react to that message and the application doesn't have to know about them. .. note:: This approach is not mandatory and it's up to you to decide how to design your entities interaction. Implementation details ~~~~~~~~~~~~~~~~~~~~~~ CherryPy's bus implementation is simplistic as it registers functions to channels. Whenever a message is published to a channel, each registered function is applied with that message passed as a parameter. The whole behaviour happens synchronously and, in that sense, if a subscriber takes too long to process a message, the remaining subscribers will be delayed. CherryPy's bus is not an advanced pubsub messaging broker system such as provided by `zeromq `_ or `RabbitMQ `_. Use it with the understanding that it may have a cost. .. _cpengine: Engine as a pubsub bus ~~~~~~~~~~~~~~~~~~~~~~ As said earlier, CherryPy is built around a pubsub bus. All entities that the framework manages at runtime are working on top of a single bus instance, which is named the `engine`. The bus implementation therefore provides a set of common channels which describe the application's lifecycle: .. code-block:: text O | V STOPPING --> STOPPED --> EXITING -> X A A | | \___ | | \ | | V V STARTED <-- STARTING The states' transitions trigger channels to be published to so that subscribers can react to them. One good example is the HTTP server which will tranisition from a `"STOPPED"` stated to a `"STARTED"` state whenever a message is published to the `start` channel. Built-in channels ~~~~~~~~~~~~~~~~~ In order to support its life-cycle, CherryPy defines a set of common channels that will be published to at various states: - **"start"**: When the bus is in the `"STARTING"` state - **"main"**: Periodically from the CherryPy's mainloop - **"stop"**: When the bus is in the `"STOPPING"` state - **"graceful"**: When the bus requests a reload of subscribers - **"exit"**: When the bus is in the `"EXITING"` state This channel will be published to by the `engine` automatically. Register therefore any subscribers that would need to react to the transition changes of the `engine`. In addition, a few other channels are also published to during the request processing. - **"before_request"**: right before the request is processed by CherryPy - **"after_request"**: right after it has been processed Also, from the :class:`cherrypy.process.plugins.ThreadManager` plugin: - **"acquire_thread"** - **"start_thread"** - **"stop_thread"** - **"release_thread"** Bus API ~~~~~~~ In order to work with the bus, the implementation provides the following simple API: - :meth:`cherrypy.engine.publish(channel, *args) `: - The `channel` parameter is a string identifying the channel to which the message should be sent to - `*args` is the message and may contain any valid Python values or objects. - :meth:`cherrypy.engine.subscribe(channel, callable) `: - The `channel` parameter is a string identifying the channel the `callable` will be registered to. - `callable` is a Python function or method which signature must match what will be published. - :meth:`cherrypy.engine.unsubscribe(channel, callable) `: - The `channel` parameter is a string identifying the channel the `callable` was registered to. - `callable` is the Python function or method which was registered. .. _busplugins: Plugins ^^^^^^^ Plugins, simply put, are entities that play with the bus, either by publishing or subscribing to channels, usually both at the same time. .. important:: Plugins are extremely useful whenever you have functionalities: - Available across the whole application server - Associated to the application's life-cycle - You want to avoid being strongly coupled to the application Create a plugin ~~~~~~~~~~~~~~~ A typical plugin looks like this: .. code-block:: python import cherrypy from cherrypy.process import wspbus, plugins class DatabasePlugin(plugins.SimplePlugin): def __init__(self, bus, db_klass): plugins.SimplePlugin.__init__(self, bus) self.db = db_klass() def start(self): self.bus.log('Starting up DB access') self.bus.subscribe("db-save", self.save_it) def stop(self): self.bus.log('Stopping down DB access') self.bus.unsubscribe("db-save", self.save_it) def save_it(self, entity): self.db.save(entity) The :class:`cherrypy.process.plugins.SimplePlugin` is a helper class provided by CherryPy that will automatically subscribe your `start` and `stop` methods to the related channels. When the `start` and `stop` channels are published on, those methods are called accordingly. Notice then how our plugin subscribes to the `db-save` channel so that the bus can dispatch messages to the plugin. Enable a plugin ~~~~~~~~~~~~~~~ To enable the plugin, it has to be registered to the the bus as follows: .. code-block:: python DatabasePlugin(cherrypy.engine, SQLiteDB).subscribe() The `SQLiteDB` here is a fake class that is used as our database provider. Disable a plugin ~~~~~~~~~~~~~~~~ You can also unregister a plugin as follows: .. code-block:: python someplugin.unsubscribe() This is often used when you want to prevent the default HTTP server from being started by CherryPy, for instance if you run on top of a different HTTP server (WSGI capable): .. code-block:: python cherrypy.server.unsubscribe() Let's see an example using this default application: .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def index(self): return "hello world" if __name__ == '__main__': cherrypy.quickstart(Root()) For instance, this is what you would see when running this application: .. code-block:: python [27/Apr/2014:13:04:07] ENGINE Listening for SIGHUP. [27/Apr/2014:13:04:07] ENGINE Listening for SIGTERM. [27/Apr/2014:13:04:07] ENGINE Listening for SIGUSR1. [27/Apr/2014:13:04:07] ENGINE Bus STARTING [27/Apr/2014:13:04:07] ENGINE Started monitor thread 'Autoreloader'. [27/Apr/2014:13:04:08] ENGINE Serving on http://127.0.0.1:8080 [27/Apr/2014:13:04:08] ENGINE Bus STARTED Now let's unsubscribe the HTTP server: .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def index(self): return "hello world" if __name__ == '__main__': cherrypy.server.unsubscribe() cherrypy.quickstart(Root()) This is what we get: .. code-block:: python [27/Apr/2014:13:08:06] ENGINE Listening for SIGHUP. [27/Apr/2014:13:08:06] ENGINE Listening for SIGTERM. [27/Apr/2014:13:08:06] ENGINE Listening for SIGUSR1. [27/Apr/2014:13:08:06] ENGINE Bus STARTING [27/Apr/2014:13:08:06] ENGINE Started monitor thread 'Autoreloader'. [27/Apr/2014:13:08:06] ENGINE Bus STARTED As you can see, the server is not started. The missing: .. code-block:: python [27/Apr/2014:13:04:08] ENGINE Serving on http://127.0.0.1:8080 Per-request functions ##################### One of the most common task in a web application development is to tailor the request's processing to the runtime context. Within CherryPy, this is performed via what are called `tools`. If you are familiar with Django or WSGI middlewares, CherryPy tools are similar in spirit. They add functions that are applied during the request/response processing. .. _hookpoint: Hook point ^^^^^^^^^^ A hook point is a point during the request/response processing. Here is a quick rundown of the "hook points" that you can hang your tools on: * **"on_start_resource"** - The earliest hook; the Request-Line and request headers have been processed and a dispatcher has set request.handler and request.config. * **"before_request_body"** - Tools that are hooked up here run right before the request body would be processed. * **"before_handler"** - Right before the request.handler (the :term:`exposed` callable that was found by the dispatcher) is called. * **"before_finalize"** - This hook is called right after the page handler has been processed and before CherryPy formats the final response object. It helps you for example to check for what could have been returned by your page handler and change some headers if needed. * **"on_end_resource"** - Processing is complete - the response is ready to be returned. This doesn't always mean that the request.handler (the exposed page handler) has executed! It may be a generator. If your tool absolutely needs to run after the page handler has produced the response body, you need to either use on_end_request instead, or wrap the response.body in a generator which applies your tool as the response body is being generated. * **"before_error_response"** - Called right before an error response (status code, body) is set. * **"after_error_response"** - Called right after the error response (status code, body) is set and just before the error response is finalized. * **"on_end_request"** - The request/response conversation is over, all data has been written to the client, nothing more to see here, move along. .. _tools: Tools ^^^^^ A tool is a simple callable object (function, method, object implementing a `__call__` method) that is attached to a :ref:`hook point `. Below is a simple tool that is attached to the `before_finalize` hook point, hence after the page handler was called: .. code-block:: python @cherrypy.tools.register('before_finalize') def logit(): print(cherrypy.request.remote.ip) Tools can also be created and assigned manually. The decorator registration is equivalent to: .. code-block:: python cherrypy.tools.logit = cherrypy.Tool('before_finalize', logit) Using that tool is as simple as follows: .. code-block:: python class Root(object): @cherrypy.expose @cherrypy.tools.logit() def index(self): return "hello world" Obviously the tool may be declared the :ref:`other usual ways `. .. note:: The name of the tool, technically the attribute set to `cherrypy.tools`, does not have to match the name of the callable. However, it is that name that will be used in the configuration to refer to that tool. Stateful tools ~~~~~~~~~~~~~~ The tools mechanism is really flexible and enables rich per-request functionalities. Straight tools as shown in the previous section are usually good enough. However, if your workflow requires some sort of state during the request processing, you will probably want a class-based approach: .. code-block:: python import time import cherrypy class TimingTool(cherrypy.Tool): def __init__(self): cherrypy.Tool.__init__(self, 'before_handler', self.start_timer, priority=95) def _setup(self): cherrypy.Tool._setup(self) cherrypy.request.hooks.attach('before_finalize', self.end_timer, priority=5) def start_timer(self): cherrypy.request._time = time.time() def end_timer(self): duration = time.time() - cherrypy.request._time cherrypy.log("Page handler took %.4f" % duration) cherrypy.tools.timeit = TimingTool() This tool computes the time taken by the page handler for a given request. It stores the time at which the handler is about to get called and logs the time difference right after the handler returned its result. The import bits is that the :class:`cherrypy.Tool ` constructor allows you to register to a hook point but, to attach the same tool to a different hook point, you must use the :meth:`cherrypy.request.hooks.attach ` method. The :meth:`cherrypy.Tool._setup ` method is automatically called by CherryPy when the tool is applied to the request. Next, let's see how to use our tool: .. code-block:: python class Root(object): @cherrypy.expose @cherrypy.tools.timeit() def index(self): return "hello world" Tools ordering ~~~~~~~~~~~~~~ Since you can register many tools at the same hookpoint, you may wonder in which order they will be applied. CherryPy offers a deterministic, yet so simple, mechanism to do so. Simply set the **priority** attribute to a value from 1 to 100, lower values providing greater priority. If you set the same priority for several tools, they will be called in the order you declare them in your configuration. Toolboxes ~~~~~~~~~ All of the builtin CherryPy tools are collected into a Toolbox called :attr:`cherrypy.tools`. It responds to config entries in the ``"tools"`` :ref:`namespace`. You can add your own Tools to this Toolbox as described above. You can also make your own Toolboxes if you need more modularity. For example, you might create multiple Tools for working with JSON, or you might publish a set of Tools covering authentication and authorization from which everyone could benefit (hint, hint). Creating a new Toolbox is as simple as: .. code-block:: python import cherrypy # Create a new Toolbox. newauthtools = cherrypy._cptools.Toolbox("newauth") # Add a Tool to our new Toolbox. @newauthtools.register('before_request_body') def check_access(default=False): if not getattr(cherrypy.request, "userid", default): raise cherrypy.HTTPError(401) Then, in your application, use it just like you would use ``cherrypy.tools``, with the additional step of registering your toolbox with your app. Note that doing so automatically registers the ``"newauth"`` config namespace; you can see the config entries in action below: .. code-block:: python import cherrypy class Root(object): @cherrypy.expose def default(self): return "Hello" conf = { '/demo': { 'newauth.check_access.on': True, 'newauth.check_access.default': True, } } app = cherrypy.tree.mount(Root(), config=conf) Request parameters manipulation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ HTTP uses strings to carry data between two endpoints. However your application may make better use of richer object types. As it wouldn't be really readable, nor a good idea regarding maintenance, to let each page handler deserialize data, it's a common pattern to delegate this functions to tools. For instance, let's assume you have a user id in the query-string and some user data stored into a database. You could retrieve the data, create an object and pass it on to the page handler instead of the user id. .. code-block:: python import cherrypy class UserManager(cherrypy.Tool): def __init__(self): cherrypy.Tool.__init__(self, 'before_handler', self.load, priority=10) def load(self): req = cherrypy.request # let's assume we have a db session # attached to the request somehow db = req.db # retrieve the user id and remove it # from the request parameters user_id = req.params.pop('user_id') req.params['user'] = db.get(int(user_id)) cherrypy.tools.user = UserManager() class Root(object): @cherrypy.expose @cherrypy.tools.user() def index(self, user): return "hello %s" % user.name In other words, CherryPy give you the power to: - inject data, that wasn't part of the initial request, into the page handler - remove data as well - convert data into a different, more useful, object to remove that burden from the page handler itself .. _dispatchers: Tailored dispatchers #################### Dispatching is the art of locating the appropriate page handler for a given request. Usually, dispatching is based on the request's URL, the query-string and, sometimes, the request's method (GET, POST, etc.). Based on this, CherryPy comes with various dispatchers already. In some cases however, you will need a little more. Here is an example of dispatcher that will always ensure the incoming URL leads to a lower-case page handler. .. code-block:: python import random import string import cherrypy from cherrypy._cpdispatch import Dispatcher class StringGenerator(object): @cherrypy.expose def generate(self, length=8): return ''.join(random.sample(string.hexdigits, int(length))) class ForceLowerDispatcher(Dispatcher): def __call__(self, path_info): return Dispatcher.__call__(self, path_info.lower()) if __name__ == '__main__': conf = { '/': { 'request.dispatch': ForceLowerDispatcher(), } } cherrypy.quickstart(StringGenerator(), '/', conf) Once you run this snippet, go to: - http://localhost:8080/generate?length=8 - http://localhost:8080/GENerAte?length=8 In both cases, you will be led to the `generate` page handler. Without our home-made dispatcher, the second one would fail and return a 404 error (:rfc:`7231#section-6.5.4`). Tool or dispatcher? ^^^^^^^^^^^^^^^^^^^ In the previous example, why not simply use a tool? Well, the sooner a tool can be called is always after the page handler has been found. In our example, it would be already too late as the default dispatcher would have not even found a match for `/GENerAte`. A dispatcher exists mostly to determine the best page handler to serve the requested resource. On the other hand, tools are there to adapt the request's processing to the runtime context of the application and the request's content. Usually, you will have to write a dispatcher only if you have a very specific use case to locate the most adequate page handler. Otherwise, the default ones will likely suffice. Request body processors ####################### Since its 3.2 release, CherryPy provides a really elegant and powerful mechanism to deal with a request's body based on its mimetype. Refer to the :mod:`cherrypy._cpreqbody` module to understand how to implement your own processors. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/glossary.rst0000644252176402575230000000133514307416152017562 0ustar00jaracoprimarygroup Glossary -------- .. glossary:: application A CherryPy application is simply a class instance containing at least one page handler. controller Loose name commonly given to a class owning at least one exposed method exposed A Python function or method which has an attribute called `exposed` set to `True`. This attribute can be set directly or via the :func:`cherrypy.expose()` decorator. .. code-block:: python @cherrypy.expose def method(...): ... is equivalent to: .. code-block:: python def method(...): ... method.exposed = True page handler Name commonly given to an exposed method ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/history.rst0000644252176402575230000000010314307416152017410 0ustar00jaracoprimarygroup:tocdepth: 1 History ======= .. include:: ../CHANGES (links).rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/docs/index.rst0000644252176402575230000000316714424762003017032 0ustar00jaracoprimarygroup CherryPy — A Minimalist Python Web Framework ============================================ .. toctree:: :hidden: intro.rst install.rst tutorials.rst basics.rst advanced.rst config.rst extend.rst deploy.rst support.rst enterprise.rst contribute.rst development.rst glossary.rst history.rst .. toctree:: :hidden: :caption: Reference pkg/modules `CherryPy `_ is a pythonic, object-oriented web framework. CherryPy allows developers to build web applications in much the same way they would build any other object-oriented Python program. This results in smaller source code developed in less time. CherryPy is now more than ten years old, and it has proven to be fast and reliable. It is being used in production by many sites, from the simplest to the most demanding. A CherryPy application typically looks like this: .. code-block:: python import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello World!" cherrypy.quickstart(HelloWorld()) In order to make the most of CherryPy, you should start with the :ref:`tutorials ` that will lead you through the most common aspects of the framework. Once done, you will probably want to browse through the :ref:`basics ` and :ref:`advanced ` sections that will demonstrate how to implement certain operations. Finally, you will want to carefully read the configuration and :ref:`extend ` sections that go in-depth regarding the powerful features provided by the framework. Above all, have fun with your application! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/install.rst0000644252176402575230000000762114307416152017371 0ustar00jaracoprimarygroup Installation ------------ |project| is a pure Python library. This has various consequences: - It can run anywhere Python runs - It does not require a C compiler - It can run on various implementations of the Python language: `CPython `_, `IronPython `_, `Jython `_ and `PyPy `_ .. contents:: :depth: 4 Requirements ############ |project| does not have any mandatory env requirements. Python-based distribution requirements are installed automatically by ``pip``. However certain features it comes with will require you install certain packages. To simplify installing additional dependencies |project| enables you to specify extras in your requirements (e.g. ``cherrypy[json,routes_dispatcher,ssl]``): - doc -- for documentation related stuff - json -- for custom `JSON processing library `_ - routes_dispatcher -- `routes `_ for declarative URL mapping dispatcher - ssl -- for `OpenSSL bindings `_, useful in Python environments not having the builtin :mod:`ssl` module - testing - memcached_session -- enables `memcached `_ backend session - xcgi Supported python version ######################## |project| supports Python |min_py_supported| through to |max_py_supported|. Installing ########## |project| can be easily installed via common Python package managers such as setuptools or pip. .. code-block:: bash $ easy_install cherrypy .. code-block:: bash $ pip install cherrypy You may also get the latest |project| version by grabbing the source code from Github: .. code-block:: bash $ git clone https://github.com/cherrypy/cherrypy $ cd cherrypy $ python setup.py install Test your installation ^^^^^^^^^^^^^^^^^^^^^^ |project| comes with a set of simple tutorials that can be executed once you have deployed the package. .. code-block:: bash $ python -m cherrypy.tutorial.tut01_helloworld Point your browser at http://127.0.0.1:8080 and enjoy the magic. Once started the above command shows the following logs: .. code-block:: bash [15/Feb/2014:21:51:22] ENGINE Listening for SIGHUP. [15/Feb/2014:21:51:22] ENGINE Listening for SIGTERM. [15/Feb/2014:21:51:22] ENGINE Listening for SIGUSR1. [15/Feb/2014:21:51:22] ENGINE Bus STARTING [15/Feb/2014:21:51:22] ENGINE Started monitor thread 'Autoreloader'. [15/Feb/2014:21:51:22] ENGINE Serving on http://127.0.0.1:8080 [15/Feb/2014:21:51:23] ENGINE Bus STARTED We will explain what all those lines mean later on, but suffice to know that once you see the last two lines, your server is listening and ready to receive requests. Run it ###### During development, the easiest path is to run your application as follow: .. code-block:: bash $ python myapp.py As long as `myapp.py` defines a `"__main__"` section, it will run just fine. cherryd ^^^^^^^ Another way to run the application is through the ``cherryd`` script which is installed along side |project|. .. note:: This utility command will not concern you if you embed your application with another framework. Command-Line Options ~~~~~~~~~~~~~~~~~~~~ .. program:: cherryd .. cmdoption:: -c, --config Specify config file(s) .. cmdoption:: -d Run the server as a daemon .. cmdoption:: -e, --environment Apply the given config environment (defaults to None) .. index:: FastCGI .. cmdoption:: -f Start a :ref:`FastCGI ` server instead of the default HTTP server .. index:: SCGI .. cmdoption:: -s Start a SCGI server instead of the default HTTP server .. cmdoption:: -i, --import Specify modules to import .. index:: PID file .. cmdoption:: -p, --pidfile Store the process id in the given file (defaults to None) .. cmdoption:: -P, --Path Add the given paths to sys.path ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/intro.rst0000644252176402575230000002345614307416152017062 0ustar00jaracoprimarygroupForeword -------- Why CherryPy? ############# CherryPy is among the oldest web framework available for Python, yet many people aren't aware of its existence. One of the reason for this is that CherryPy is not a complete stack with built-in support for a multi-tier architecture. It doesn't provide frontend utilities nor will it tell you how to speak with your storage. Instead, CherryPy's take is to let the developer make those decisions. This is a contrasting position compared to other well-known frameworks. CherryPy has a clean interface and does its best to stay out of your way whilst providing a reliable scaffolding for you to build from. Typical use-cases for CherryPy go from regular web application with user frontends (think blogging, CMS, portals, ecommerce) to web-services only. Here are some reasons you would want to choose CherryPy: 1. Simplicity Developing with CherryPy is a simple task. “Hello, world” is only a few lines long, and does not require the developer to learn the entire (albeit very manageable) framework all at once. The framework is very pythonic; that is, it follows Python’s conventions very nicely (code is sparse and clean). Contrast this with J2EE and Python’s most popular and visible web frameworks: Django, Zope, Pylons, and Turbogears. In all of them, the learning curve is massive. In these frameworks, “Hello, world” requires the programmer to set up a large scaffold which spans multiple files and to type a lot of boilerplate code. CherryPy succeeds because it does not include the bloat of other frameworks, allowing the programmer to write their web application quickly while still maintaining a high level of organization and scalability. CherryPy is also very modular. The core is fast and clean, and extension features are easy to write and plug in using code or the elegant config system. The primary components (server, engine, request, response, etc.) are all extendable (even replaceable) and well-managed. In short, CherryPy empowers the developer to work with the framework, not against or around it. 2. Power CherryPy leverages all of the power of Python. Python is a dynamic language which allows for rapid development of applications. Python also has an extensive built-in API which simplifies web app development. Even more extensive, however, are the third-party libraries available for Python. These range from object-relational mappers to form libraries, to an automatic Python optimizer, a Windows exe generator, imaging libraries, email support, HTML templating engines, etc. CherryPy applications are just like regular Python applications. CherryPy does not stand in your way if you want to use these brilliant tools. CherryPy also provides :ref:`tools ` and :ref:`plugins `, which are powerful extension points needed to develop world-class web applications. 3. Maturity Maturity is extremely important when developing a real-world application. Unlike many other web frameworks, CherryPy has had many final, stable releases. It is fully bugtested, optimized, and proven reliable for real-world use. The API will not suddenly change and break backwards compatibility, so your applications are assured to continue working even through subsequent updates in the current version series. CherryPy is also a “3.0” project: the first edition of CherryPy set the tone, the second edition made it work, and the third edition makes it beautiful. Each version built on lessons learned from the previous, bringing the developer a superior tool for the job. 4. Community CherryPy has an devoted community that develops deployed CherryPy applications and are willing and ready to assist you on the CherryPy mailing list or Gitter. The developers also frequent the list and often answer questions and implement features requested by the end-users. 5. Deployability Unlike many other Python web frameworks, there are cost-effective ways to deploy your CherryPy application. Out of the box, CherryPy includes its own production-ready HTTP server to host your application. CherryPy can also be deployed on any WSGI-compliant gateway (a technology for interfacing numerous types of web servers): mod_wsgi, FastCGI, SCGI, IIS, uwsgi, tornado, etc. Reverse proxying is also a common and easy way to set it up. In addition, CherryPy is pure-python and is compatible with Python 2.3. This means that CherryPy will run on all major platforms that Python will run on (Windows, MacOSX, Linux, BSD, etc). `webfaction.com `_, run by the inventor of CherryPy, is a commercial web host that offers CherryPy hosting packages (in addition to several others). 6. It’s free! All of CherryPy is licensed under the open-source BSD license, which means CherryPy can be used commercially for ZERO cost. 7. Where to go from here? Check out the :ref:`tutorials ` to start enjoying the fun! .. _successstories: Success Stories ############### You are interested in CherryPy but you would like to hear more from people using it, or simply check out products or application running it. If you would like to have your CherryPy powered website or product listed here, contact us via our `mailing list `_ or `Gitter `_. Websites running atop CherryPy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Hulu Deejay and Hulu Sod `_ - Hulu uses CherryPy for some projects. "The service needs to be very high performance. Python, together with CherryPy, `gunicorn `_, and gevent more than provides for this." `Netflix `_ - Netflix uses CherryPy as a building block in their infrastructure: "Restful APIs to large applications with requests, providing web interfaces with CherryPy and Bottle, and crunching data with scipy." `Urbanility `_ - French website for local neighbourhood assets in Rennes, France. `MROP Supply `_ - Webshop for industrial equipment, developed using CherryPy 3.2.2 utilizing Python 3.2, with libs: `Jinja2-2.6 `_, davispuh-MySQL-for-Python-3-3403794, pyenchant-1.6.5 (for search spelling). "I'm coming over from .net development and found Python and CherryPy to be surprisingly minimalistic. No unnecessary overhead - build everything you need without the extra fluff. I'm a fan!" `CherryMusic `_ - A music streaming server written in python: Stream your own music collection to all your devices! CherryMusic is open source. `YouGov Global `_ - International market research firm, conducts millions of surveys on CherryPy yearly. `Aculab Cloud `_ - Voice and fax applications on the cloud. A simple telephony API for Python, C#, C++, VB, etc... The website and all front-end and back-end web services are built with CherryPy, fronted by nginx (just handling the ssh and reverse-proxy), and running on AWS in two regions. `Learnit Training `_ - Dutch website for an IT, Management and Communication training company. Built on CherryPy 3.2.0 and Python 2.7.3, with `oursql `_ and `DBUtils `_ libraries, amongst others. `Linstic `_ - Sticky Notes in your browser (with linking). `Almad's Homepage `_ - Simple homepage with blog. `Fight.Watch `_ - Twitch.tv web portal for fighting games. Built on CherryPy 3.3.0 and Python 2.7.3 with Jinja 2.7.2 and SQLAlchemy 0.9.4. Products based on CherryPy ^^^^^^^^^^^^^^^^^^^^^^^^^^ `SABnzbd `_ - Open Source Binary Newsreader written in Python. `Headphones `_ - Third-party add-on for SABnzbd. `SickBeard `_ - "Sick Beard is a PVR for newsgroup users (with limited torrent support). It watches for new episodes of your favorite shows and when they are posted it downloads them, sorts and renames them, and optionally generates metadata for them." `TurboGears `_ - The rapid web development megaframework. Turbogears 1.x used Cherrypy. "CherryPy is the underlying application server for TurboGears. It is responsible for taking the requests from the user’s browser, parses them and turns them into calls into the Python code of the web application. Its role is similar to application servers used in other programming languages". `Indigo `_ - "An intelligent home control server that integrates home control hardware modules to provide control of your home. Indigo's built-in Web server and client/server architecture give you control and access to your home remotely from other Macs, PCs, internet tablets, PDAs, and mobile phones." `SlikiWiki `_ - Wiki built on CherryPy and featuring WikiWords, automatic backlinking, site map generation, full text search, locking for concurrent edits, RSS feed embedding, per page access control lists, and page formatting using PyTextile markup." `read4me `_ - read4me is a Python feed-reading web service. `Firebird QA tools `_ - Firebird QA tools are based on CherryPy. `salt-api `_ - A REST API for Salt, the infrastructure orchestration tool. Products inspired by CherryPy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `OOWeb `_ - "OOWeb is a lightweight, embedded HTTP server for Java applications that maps objects to URL directories, methods to pages and form/querystring arguments as method parameters. OOWeb was originally inspired by CherryPy." ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1702477980.550681 CherryPy-18.9.0/docs/pkg/0000755252176402575230000000000014536340235015746 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/pkg/.gitignore0000644252176402575230000000001614307416152017731 0ustar00jaracoprimarygroup* !.gitignore ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/support.rst0000644252176402575230000000004414307416152017427 0ustar00jaracoprimarygroup.. include:: ../.github/SUPPORT.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/docs/tutorials.rst0000644252176402575230000011776714307416152017766 0ustar00jaracoprimarygroup.. _tutorials: Tutorials --------- This tutorial will walk you through basic but complete CherryPy applications that will show you common concepts as well as slightly more advanced ones. .. contents:: :depth: 4 Tutorial 1: A basic web application ################################### The following example demonstrates the most basic application you could write with CherryPy. It starts a server and hosts an application that will be served at request reaching http://127.0.0.1:8080/ .. code-block:: python :linenos: import cherrypy class HelloWorld(object): @cherrypy.expose def index(self): return "Hello world!" if __name__ == '__main__': cherrypy.quickstart(HelloWorld()) Store this code snippet into a file named `tut01.py` and execute it as follows: .. code-block:: bash $ python tut01.py This will display something along the following: .. code-block:: text :linenos: [24/Feb/2014:21:01:46] ENGINE Listening for SIGHUP. [24/Feb/2014:21:01:46] ENGINE Listening for SIGTERM. [24/Feb/2014:21:01:46] ENGINE Listening for SIGUSR1. [24/Feb/2014:21:01:46] ENGINE Bus STARTING CherryPy Checker: The Application mounted at '' has an empty config. [24/Feb/2014:21:01:46] ENGINE Started monitor thread 'Autoreloader'. [24/Feb/2014:21:01:46] ENGINE Serving on http://127.0.0.1:8080 [24/Feb/2014:21:01:46] ENGINE Bus STARTED This tells you several things. The first three lines indicate the server will handle :mod:`signal` for you. The next line tells you the current state of the server, as that point it is in `STARTING` stage. Then, you are notified your application has no specific configuration set to it. Next, the server starts a couple of internal utilities that we will explain later. Finally, the server indicates it is now ready to accept incoming communications as it listens on the address `127.0.0.1:8080`. In other words, at that stage your application is ready to be used. Before moving on, let's discuss the message regarding the lack of configuration. By default, CherryPy has a feature which will review the syntax correctness of settings you could provide to configure the application. When none are provided, a warning message is thus displayed in the logs. That log is harmless and will not prevent CherryPy from working. You can refer to :ref:`the documentation above ` to understand how to set the configuration. .. _tut02: Tutorial 2: Different URLs lead to different functions ###################################################### Your applications will obviously handle more than a single URL. Let's imagine you have an application that generates a random string each time it is called: .. code-block:: python :linenos: import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return "Hello world!" @cherrypy.expose def generate(self): return ''.join(random.sample(string.hexdigits, 8)) if __name__ == '__main__': cherrypy.quickstart(StringGenerator()) Save this into a file named `tut02.py` and run it as follows: .. code-block:: bash $ python tut02.py Go now to http://localhost:8080/generate and your browser will display a random string. Let's take a minute to decompose what's happening here. This is the URL that you have typed into your browser: http://localhost:8080/generate This URL contains various parts: - `http://` which roughly indicates it's a URL using the HTTP protocol (see :rfc:`2616`). - `localhost:8080` is the server's address. It's made of a hostname and a port. - `/generate` which is the path segment of the URL. This is what CherryPy uses to locate an :term:`exposed` function or method to respond. Here CherryPy uses the `index()` method to handle `/` and the `generate()` method to handle `/generate` .. _tut03: Tutorial 3: My URLs have parameters ################################### In the previous tutorial, we have seen how to create an application that could generate a random string. Let's now assume you wish to indicate the length of that string dynamically. .. code-block:: python :linenos: import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return "Hello world!" @cherrypy.expose def generate(self, length=8): return ''.join(random.sample(string.hexdigits, int(length))) if __name__ == '__main__': cherrypy.quickstart(StringGenerator()) Save this into a file named `tut03.py` and run it as follows: .. code-block:: bash $ python tut03.py Go now to http://localhost:8080/generate?length=16 and your browser will display a generated string of length 16. Notice how we benefit from Python's default arguments' values to support URLs such as http://localhost:8080/generate still. In a URL such as this one, the section after `?` is called a query-string. Traditionally, the query-string is used to contextualize the URL by passing a set of (key, value) pairs. The format for those pairs is `key=value`. Each pair being separated by a `&` character. Notice how we have to convert the given `length` value to an integer. Indeed, values are sent out from the client to our server as strings. Much like CherryPy maps URL path segments to exposed functions, query-string keys are mapped to those exposed function parameters. .. _tut04: Tutorial 4: Submit this form ############################ CherryPy is a web framework upon which you build web applications. The most traditional shape taken by applications is through an HTML user-interface speaking to your CherryPy server. Let's see how to handle HTML forms via the following example. .. code-block:: python :linenos: import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return """
""" @cherrypy.expose def generate(self, length=8): return ''.join(random.sample(string.hexdigits, int(length))) if __name__ == '__main__': cherrypy.quickstart(StringGenerator()) Save this into a file named `tut04.py` and run it as follows: .. code-block:: bash $ python tut04.py Go now to http://localhost:8080/ and your browser and this will display a simple input field to indicate the length of the string you want to generate. Notice that in this example, the form uses the `GET` method and when you pressed the `Give it now!` button, the form is sent using the same URL as in the :ref:`previous ` tutorial. HTML forms also support the `POST` method, in that case the query-string is not appended to the URL but it sent as the body of the client's request to the server. However, this would not change your application's exposed method because CherryPy handles both the same way and uses the exposed's handler parameters to deal with the query-string (key, value) pairs. .. _tut05: Tutorial 5: Track my end-user's activity ######################################## It's not uncommon that an application needs to follow the user's activity for a while. The usual mechanism is to use a `session identifier `_ that is carried during the conversation between the user and your application. .. code-block:: python :linenos: import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return """
""" @cherrypy.expose def generate(self, length=8): some_string = ''.join(random.sample(string.hexdigits, int(length))) cherrypy.session['mystring'] = some_string return some_string @cherrypy.expose def display(self): return cherrypy.session['mystring'] if __name__ == '__main__': conf = { '/': { 'tools.sessions.on': True } } cherrypy.quickstart(StringGenerator(), '/', conf) Save this into a file named `tut05.py` and run it as follows: .. code-block:: bash $ python tut05.py In this example, we generate the string as in the :ref:`previous ` tutorial but also store it in the current session. If you go to http://localhost:8080/, generate a random string, then go to http://localhost:8080/display, you will see the string you just generated. The lines 30-34 show you how to enable the session support in your CherryPy application. By default, CherryPy will save sessions in the process's memory. It supports more persistent :ref:`backends ` as well. Tutorial 6: What about my javascripts, CSS and images? ###################################################### Web applications are usually also made of static content such as javascript, CSS files or images. CherryPy provides support to serve static content to end-users. Let's assume, you want to associate a stylesheet with your application to display a blue background color (why not?). First, save the following stylesheet into a file named `style.css` and stored into a local directory `public/css`. .. code-block:: css :linenos: body { background-color: blue; } Now let's update the HTML code so that we link to the stylesheet using the http://localhost:8080/static/css/style.css URL. .. code-block:: python :linenos: import os, os.path import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return """
""" @cherrypy.expose def generate(self, length=8): some_string = ''.join(random.sample(string.hexdigits, int(length))) cherrypy.session['mystring'] = some_string return some_string @cherrypy.expose def display(self): return cherrypy.session['mystring'] if __name__ == '__main__': conf = { '/': { 'tools.sessions.on': True, 'tools.staticdir.root': os.path.abspath(os.getcwd()) }, '/static': { 'tools.staticdir.on': True, 'tools.staticdir.dir': './public' } } cherrypy.quickstart(StringGenerator(), '/', conf) Save this into a file named `tut06.py` and run it as follows: .. code-block:: bash $ python tut06.py Going to http://localhost:8080/, you should be greeted by a flashy blue color. CherryPy provides support to serve a single file or a complete directory structure. Most of the time, this is what you'll end up doing so this is what the code above demonstrates. First, we indicate the `root` directory of all of our static content. This must be an absolute path for security reason. CherryPy will complain if you provide only relative paths when looking for a match to your URLs. Then we indicate that all URLs which path segment starts with `/static` will be served as static content. We map that URL to the `public` directory, a direct child of the `root` directory. The entire sub-tree of the `public` directory will be served as static content. CherryPy will map URLs to path within that directory. This is why `/static/css/style.css` is found in `public/css/style.css`. Tutorial 7: Give us a REST ########################## It's not unusual nowadays that web applications expose some sort of datamodel or computation functions. Without going into its details, one strategy is to follow the `REST principles edicted by Roy T. Fielding `_. Roughly speaking, it assumes that you can identify a resource and that you can address that resource through that identifier. "What for?" you may ask. Well, mostly, these principles are there to ensure that you decouple, as best as you can, the entities your application expose from the way they are manipulated or consumed. To embrace this point of view, developers will usually design a web API that expose pairs of `(URL, HTTP method, data, constraints)`. .. note:: You will often hear REST and web API together. The former is one strategy to provide the latter. This tutorial will not go deeper in that whole web API concept as it's a much more engaging subject, but you ought to read more about it online. Lets go through a small example of a very basic web API mildly following REST principles. .. code-block:: python :linenos: import random import string import cherrypy @cherrypy.expose class StringGeneratorWebService(object): @cherrypy.tools.accept(media='text/plain') def GET(self): return cherrypy.session['mystring'] def POST(self, length=8): some_string = ''.join(random.sample(string.hexdigits, int(length))) cherrypy.session['mystring'] = some_string return some_string def PUT(self, another_string): cherrypy.session['mystring'] = another_string def DELETE(self): cherrypy.session.pop('mystring', None) if __name__ == '__main__': conf = { '/': { 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), 'tools.sessions.on': True, 'tools.response_headers.on': True, 'tools.response_headers.headers': [('Content-Type', 'text/plain')], } } cherrypy.quickstart(StringGeneratorWebService(), '/', conf) Save this into a file named `tut07.py` and run it as follows: .. code-block:: bash $ python tut07.py Before we see it in action, let's explain a few things. Until now, CherryPy was creating a tree of exposed methods that were used to match URLs. In the case of our web API, we want to stress the role played by the actual requests' HTTP methods. So we created methods that are named after them and they are all exposed at once by decorating the class itself with `cherrypy.expose`. However, we must then switch from the default mechanism of matching URLs to method for one that is aware of the whole HTTP method shenanigan. This is what goes on line 27 where we create a :class:`~cherrypy.dispatch.MethodDispatcher` instance. Then we force the responses `content-type` to be `text/plain` and we finally ensure that `GET` requests will only be responded to clients that accept that `content-type` by having a `Accept: text/plain` header set in their request. However, we do this only for that HTTP method as it wouldn't have much meaning on the other methods. For the purpose of this tutorial, we will be using a Python client rather than your browser as we wouldn't be able to actually try our web API otherwise. Please install `requests `_ through the following command: .. code-block:: bash $ pip install requests Then fire up a Python terminal and try the following commands: .. code-block:: pycon :linenos: >>> import requests >>> s = requests.Session() >>> r = s.get('http://127.0.0.1:8080/') >>> r.status_code 500 >>> r = s.post('http://127.0.0.1:8080/') >>> r.status_code, r.text (200, u'04A92138') >>> r = s.get('http://127.0.0.1:8080/') >>> r.status_code, r.text (200, u'04A92138') >>> r = s.get('http://127.0.0.1:8080/', headers={'Accept': 'application/json'}) >>> r.status_code 406 >>> r = s.put('http://127.0.0.1:8080/', params={'another_string': 'hello'}) >>> r = s.get('http://127.0.0.1:8080/') >>> r.status_code, r.text (200, u'hello') >>> r = s.delete('http://127.0.0.1:8080/') >>> r = s.get('http://127.0.0.1:8080/') >>> r.status_code 500 The first and last `500` responses stem from the fact that, in the first case, we haven't yet generated a string through `POST` and, on the latter case, that it doesn't exist after we've deleted it. Lines 12-14 show you how the application reacted when our client requested the generated string as a JSON format. Since we configured the web API to only support plain text, it returns the appropriate `HTTP error code `_. .. note:: We use the `Session `_ interface of `requests` so that it takes care of carrying the session id stored in the request cookie in each subsequent request. That is handy. .. important:: It's all about RESTful URLs these days, isn't it? It is likely your URL will be made of dynamic parts that you will not be able to match to page handlers. For example, ``/library/12/book/15`` cannot be directly handled by the default CherryPy dispatcher since the segments ``12`` and ``15`` will not be matched to any Python callable. This can be easily workaround with two handy CherryPy features explained in the :ref:`advanced section `. .. _tut08: Tutorial 8: Make it smoother with Ajax ###################################### In the recent years, web applications have moved away from the simple pattern of "HTML forms + refresh the whole page". This traditional scheme still works very well but users have become used to web applications that don't refresh the entire page. Broadly speaking, web applications carry code performed client-side that can speak with the backend without having to refresh the whole page. This tutorial will involve a little more code this time around. First, let's see our CSS stylesheet located in `public/css/style.css`. .. code-block:: css :linenos: body { background-color: blue; } #the-string { display: none; } We're adding a simple rule about the element that will display the generated string. By default, let's not show it up. Save the following HTML code into a file named `index.html`. .. code-block:: html :linenos:
We'll be using the `jQuery framework `_ out of simplicity but feel free to replace it with your favourite tool. The page is composed of simple HTML elements to get user input and display the generated string. It also contains client-side code to talk to the backend API that actually performs the hard work. Finally, here's the application's code that serves the HTML page above and responds to requests to generate strings. Both are hosted by the same application server. .. code-block:: python :linenos: import os, os.path import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return open('index.html') @cherrypy.expose class StringGeneratorWebService(object): @cherrypy.tools.accept(media='text/plain') def GET(self): return cherrypy.session['mystring'] def POST(self, length=8): some_string = ''.join(random.sample(string.hexdigits, int(length))) cherrypy.session['mystring'] = some_string return some_string def PUT(self, another_string): cherrypy.session['mystring'] = another_string def DELETE(self): cherrypy.session.pop('mystring', None) if __name__ == '__main__': conf = { '/': { 'tools.sessions.on': True, 'tools.staticdir.root': os.path.abspath(os.getcwd()) }, '/generator': { 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), 'tools.response_headers.on': True, 'tools.response_headers.headers': [('Content-Type', 'text/plain')], }, '/static': { 'tools.staticdir.on': True, 'tools.staticdir.dir': './public' } } webapp = StringGenerator() webapp.generator = StringGeneratorWebService() cherrypy.quickstart(webapp, '/', conf) Save this into a file named `tut08.py` and run it as follows: .. code-block:: bash $ python tut08.py Go to http://127.0.0.1:8080/ and play with the input and buttons to generate, replace or delete the strings. Notice how the page isn't refreshed, simply part of its content. Notice as well how your frontend converses with the backend using a straightfoward, yet clean, web service API. That same API could easily be used by non-HTML clients. .. _tut09: Tutorial 9: Data is all my life ############################### Until now, all the generated strings were saved in the session, which by default is stored in the process memory. Though, you can persist sessions on disk or in a distributed memory store, this is not the right way of keeping your data on the long run. Sessions are there to identify your user and carry as little amount of data as necessary for the operation carried by the user. To store, persist and query data you need a proper database server. There exist many to choose from with various paradigm support: - relational: PostgreSQL, SQLite, MariaDB, Firebird - column-oriented: HBase, Cassandra - key-store: redis, memcached - document oriented: Couchdb, MongoDB - graph-oriented: neo4j Let's focus on the relational ones since they are the most common and probably what you will want to learn first. For the sake of reducing the number of dependencies for these tutorials, we will go for the :mod:`sqlite` database which is directly supported by Python. Our application will replace the storage of the generated string from the session to a SQLite database. The application will have the same HTML code as :ref:`tutorial 08 `. So let's simply focus on the application code itself: .. code-block:: python :linenos: import os, os.path import random import sqlite3 import string import time import cherrypy DB_STRING = "my.db" class StringGenerator(object): @cherrypy.expose def index(self): return open('index.html') @cherrypy.expose class StringGeneratorWebService(object): @cherrypy.tools.accept(media='text/plain') def GET(self): with sqlite3.connect(DB_STRING) as c: cherrypy.session['ts'] = time.time() r = c.execute("SELECT value FROM user_string WHERE session_id=?", [cherrypy.session.id]) return r.fetchone() def POST(self, length=8): some_string = ''.join(random.sample(string.hexdigits, int(length))) with sqlite3.connect(DB_STRING) as c: cherrypy.session['ts'] = time.time() c.execute("INSERT INTO user_string VALUES (?, ?)", [cherrypy.session.id, some_string]) return some_string def PUT(self, another_string): with sqlite3.connect(DB_STRING) as c: cherrypy.session['ts'] = time.time() c.execute("UPDATE user_string SET value=? WHERE session_id=?", [another_string, cherrypy.session.id]) def DELETE(self): cherrypy.session.pop('ts', None) with sqlite3.connect(DB_STRING) as c: c.execute("DELETE FROM user_string WHERE session_id=?", [cherrypy.session.id]) def setup_database(): """ Create the `user_string` table in the database on server startup """ with sqlite3.connect(DB_STRING) as con: con.execute("CREATE TABLE user_string (session_id, value)") def cleanup_database(): """ Destroy the `user_string` table from the database on server shutdown. """ with sqlite3.connect(DB_STRING) as con: con.execute("DROP TABLE user_string") if __name__ == '__main__': conf = { '/': { 'tools.sessions.on': True, 'tools.staticdir.root': os.path.abspath(os.getcwd()) }, '/generator': { 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), 'tools.response_headers.on': True, 'tools.response_headers.headers': [('Content-Type', 'text/plain')], }, '/static': { 'tools.staticdir.on': True, 'tools.staticdir.dir': './public' } } cherrypy.engine.subscribe('start', setup_database) cherrypy.engine.subscribe('stop', cleanup_database) webapp = StringGenerator() webapp.generator = StringGeneratorWebService() cherrypy.quickstart(webapp, '/', conf) Save this into a file named `tut09.py` and run it as follows: .. code-block:: bash $ python tut09.py Let's first see how we create two functions that create and destroy the table within our database. These functions are registered to the CherryPy's server on lines 85-86, so that they are called when the server starts and stops. Next, notice how we replaced all the session code with calls to the database. We use the session id to identify the user's string within our database. Since the session will go away after a while, it's probably not the right approach. A better idea would be to associate the user's login or more resilient unique identifier. For the sake of our demo, this should do. .. important:: In this example, we must still set the session to a dummy value so that the session is not `discarded `_ on each request by CherryPy. Since we now use the database to store the generated string, we simply store a dummy timestamp inside the session. .. note:: Unfortunately, sqlite in Python forbids us to share a connection between threads. Since CherryPy is a multi-threaded server, this would be an issue. This is the reason why we open and close a connection to the database on each call. This is clearly not really production friendly, and it is probably advisable to either use a more capable database engine or a higher level library, such as `SQLAlchemy `_, to better support your application's needs. .. _tut10: Tutorial 10: Make it a modern single-page application with React.js ################################################################### In the recent years, client-side single-page applications (SPA) have gradually eaten server-side generated content web applications's lunch. This tutorial demonstrates how to integrate with `React.js `_, a Javascript library for SPA released by Facebook in 2013. Please refer to React.js documentation to learn more about it. To demonstrate it, let's use the code from :ref:`tutorial 09 `. However, we will be replacing the HTML and Javascript code. First, let's see how our HTML code has changed: .. code-block:: html :linenos:
Basically, we have removed the entire Javascript code that was using jQuery. Instead, we load the React.js library as well as a new, local, Javascript module, named ``gen.js`` and located in the ``public/js`` directory: .. code-block:: javascript :linenos: var StringGeneratorBox = React.createClass({ handleGenerate: function() { var length = this.state.length; this.setState(function() { $.ajax({ url: this.props.url, dataType: 'text', type: 'POST', data: { "length": length }, success: function(data) { this.setState({ length: length, string: data, mode: "edit" }); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString() ); }.bind(this) }); }); }, handleEdit: function() { var new_string = this.state.string; this.setState(function() { $.ajax({ url: this.props.url, type: 'PUT', data: { "another_string": new_string }, success: function() { this.setState({ length: new_string.length, string: new_string, mode: "edit" }); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString() ); }.bind(this) }); }); }, handleDelete: function() { this.setState(function() { $.ajax({ url: this.props.url, type: 'DELETE', success: function() { this.setState({ length: "8", string: "", mode: "create" }); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString() ); }.bind(this) }); }); }, handleLengthChange: function(length) { this.setState({ length: length, string: "", mode: "create" }); }, handleStringChange: function(new_string) { this.setState({ length: new_string.length, string: new_string, mode: "edit" }); }, getInitialState: function() { return { length: "8", string: "", mode: "create" }; }, render: function() { return (
); } }); var StringGeneratorForm = React.createClass({ handleCreate: function(e) { e.preventDefault(); this.props.onCreateString(); }, handleReplace: function(e) { e.preventDefault(); this.props.onReplaceString(); }, handleDelete: function(e) { e.preventDefault(); this.props.onDeleteString(); }, handleLengthChange: function(e) { e.preventDefault(); var length = React.findDOMNode(this.refs.length).value.trim(); this.props.onLengthChange(length); }, handleStringChange: function(e) { e.preventDefault(); var string = React.findDOMNode(this.refs.string).value.trim(); this.props.onStringChange(string); }, render: function() { if (this.props.mode == "create") { return (
); } else if (this.props.mode == "edit") { return (
); } return null; } }); React.render( , document.getElementById('generator') ); Wow! What a lot of code for something so simple, isn't it? The entry point is the last few lines where we indicate that we want to render the HTML code of the ``StringGeneratorBox`` React.js class inside the ``generator`` div. When the page is rendered, so is that component. Notice how it is also made of another component that renders the form itself. This might be a little over the top for such a simple example but hopefully will get you started with React.js in the process. There is not much to say and, hopefully, the meaning of that code is rather clear. The component has an internal `state `_ in which we store the current string as generated/modified by the user. When the user `changes the content of the input boxes `_, the state is updated on the client side. Then, when a button is clicked, that state is sent out to the backend server using the API endpoint and the appropriate action takes places. Then, the state is updated and so is the view. Tutorial 11: Organize my code ############################# CherryPy comes with a powerful architecture that helps you organizing your code in a way that should make it easier to maintain and more flexible. Several mechanisms are at your disposal, this tutorial will focus on the three main ones: - :ref:`dispatchers ` - :ref:`tools ` - :ref:`plugins ` In order to understand them, let's imagine you are at a superstore: - You have several tills and people queuing for each of them (those are your requests) - You have various sections with food and other stuff (these are your data) - Finally you have the superstore people and their daily tasks to make sure sections are always in order (this is your backend) In spite of being really simplistic, this is not far from how your application behaves. CherryPy helps you structure your application in a way that mirrors these high-level ideas. Dispatchers ^^^^^^^^^^^ Coming back to the superstore example, it is likely that you will want to perform operations based on the till: - Have a till for baskets with less than ten items - Have a till for disabled people - Have a till for pregnant women - Have a till where you can only using the store card To support these use-cases, CherryPy provides a mechanism called a :ref:`dispatcher `. A dispatcher is executed early during the request processing in order to determine which piece of code of your application will handle the incoming request. Or, to continue on the store analogy, a dispatcher will decide which till to lead a customer to. Tools ^^^^^ Let's assume your store has decided to operate a discount spree but, only for a specific category of customers. CherryPy will deal with such use case via a mechanism called a :ref:`tool `. A tool is a piece of code that runs on a per-request basis in order to perform additional work. Usually a tool is a simple Python function that is executed at a given point during the process of the request by CherryPy. Plugins ^^^^^^^ As we have seen, the store has a crew of people dedicated to manage the stock and deal with any customers' expectation. In the CherryPy world, this translates into having functions that run outside of any request life-cycle. These functions should take care of background tasks, long lived connections (such as those to a database for instance), etc. :ref:`Plugins ` are called that way because they work along with the CherryPy :ref:`engine ` and extend it with your operations. Tutorial 12: Using pytest and code coverage ########################################### Pytest ^^^^^^ Let's revisit :ref:`Tutorial 2 `. .. code-block:: python :linenos: import random import string import cherrypy class StringGenerator(object): @cherrypy.expose def index(self): return "Hello world!" @cherrypy.expose def generate(self): return ''.join(random.sample(string.hexdigits, 8)) if __name__ == '__main__': cherrypy.quickstart(StringGenerator()) Save this into a file named ``tut12.py``. Now make the test file: .. code-block:: python :linenos: import cherrypy from cherrypy.test import helper from tut12 import StringGenerator class SimpleCPTest(helper.CPWebCase): @staticmethod def setup_server(): cherrypy.tree.mount(StringGenerator(), '/', {}) def test_index(self): self.getPage("/") self.assertStatus('200 OK') def test_generate(self): self.getPage("/generate") self.assertStatus('200 OK') Save this into a file named ``test_tut12.py`` and run .. code-block:: bash $ pytest -v test_tut12.py .. note:: If you don't have `pytest `_ installed, you'll need to install it by ``pip install pytest`` We now have a neat way that we can exercise our application making tests. Adding Code Coverage ^^^^^^^^^^^^^^^^^^^^ To get code coverage, simply run .. code-block:: bash $ pytest --cov=tut12 --cov-report term-missing test_tut12.py .. note:: To `add coverage support to pytest `_, you'll need to install it by ``pip install pytest-cov`` This tells us that one line is missing. Of course it is because that is only executed when the python program is started directly. We can simply change the following lines in ``tut12.py``: .. code-block:: python :lineno-start: 17 if __name__ == '__main__': # pragma: no cover cherrypy.quickstart(StringGenerator()) When you rerun the code coverage, it should show 100% now. .. note:: When using in CI, you might want to integrate `Codecov `_, `Landscape `_ or `Coveralls `_ into your project to store and track coverage data over time. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5508416 CherryPy-18.9.0/man/0000755252176402575230000000000014536340235015010 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/man/cherryd.10000644252176402575230000001361314307416152016534 0ustar00jaracoprimarygroup.\" Man page generated from reStructuredText. .TH cherryd 1 "2009-06-15" "3.2.0" "web" .SH NAME cherryd \- Starts the CherryPy HTTP server as a daemon .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level magin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS \fBcherryd\fP [\-d] [\-f | \-s] [\-e ENV_NAME] [\-p PIDFILE_PATH] [\-P DIRPATH] [\-c CONFIG_FILE] \-i MODULE_NAME .SH DESCRIPTION \fBcherryd\fP is a Python script which starts the CherryPy webserver as a daemon. .SH OPTIONS .INDENT 0.0 .TP .BI \-c\ CONFIG_FILE ,\ \-\-config\fn= CONFIG_FILE Specifies a config file which is to be read and merged into the CherryPy site\-wide config dict. This option may be specified multiple times. For each CONFIG_FILE specified, \fBcherryd\fP will perform \fBcherrypy.config.update()\fP. .TP .B \-d Run the server as a daemon. .TP .BI \-e\ ENV_NAME ,\ \-\-environment\fn= ENV_NAME Specifies the name of an environment to be applied. An environment is a canned set of configuration entries. See \fI\%ENVIRONMENTS\fP below for a list of the built\-in environments. .TP .B \-f Start a fastcgi server instead of the default HTTP server. .TP .B \-s Start a scgi server instead of the default HTTP server. .TP .BI \-i\ MODULE_NAME ,\ \-\-import\fn= MODULE_NAME Specifies a module to import. This option may be specified multiple times. For each MODULE_NAME specified, \fBcherryd\fP will import the module. This is how you tell \fBcherryd\fP to run your application\'s startup code. For all practical purposes, \fB\-i\fP is not optional; you will always need to specify at least one module. .TP .BI \-p\ PIDFILE_PATH ,\ \-\-pidfile\fn= PIDFILE_PATH Store the process id in PIDFILE_PATH. .TP .BI \-P\ DIRPATH ,\ \-\-Path\ DIRPATH Specifies a directory to be inserted at the head of \fBsys.path\fP. DIRPATH should be an absolute path. This option may be specified multiple times. \fBcherryd\fP inserts all the specified DIRPATHs into \fBsys.path\fP before it attempts to import modules specified with \fB\-i\fP. .UNINDENT For a terse summary of the options, run \fBcherryd \-\-help\fP. .SH EXAMPLES A site\-wide configuration file \fBsite.conf\fP: .INDENT 0.0 .INDENT 3.5 [global] .br server.socket_host = "0.0.0.0" .br server.socket_port = 8008 .br engine.autoreload.on = False .br .UNINDENT .UNINDENT The application startup code in \fBstartup.py\fP: .INDENT 0.0 .INDENT 3.5 import cherrypy .br import my_controller .br cherrypy.log.error_file = \'/var/tmp/myapp\-error.log\' .br cherrypy.log.access_file = \'/var/tmp/myapp\-access.log\' .br config_root = { \'tools.encode.encoding\' : \'utf\-8\', } .br app_conf = { \'/\' : config_root } .br cherrypy.tree.mount(my_controller.Root(), script_name=\'\', config=app_conf) .br .UNINDENT .UNINDENT A corresponding \fBcherryd\fP command line: .INDENT 0.0 .INDENT 3.5 cherryd \-d \-c site.conf \-i startup \-p /var/log/cherrypy/my_app.pid .br .UNINDENT .UNINDENT .SH DROPPING PRIVILEGES If you want to serve your web application on TCP port 80 (or any port lower than 1024), the CherryPy HTTP server needs to start as root in order to bind to the port. Running a web application as root is reckless, so the application should drop privileges from root to some other user and group. The application must do this itself, as \fBcherryd\fP does not do it for you. To drop privileges, put the following lines into your startup code, substituting appropriate values for \fBumask\fP, \fBuid\fP and \fBgid\fP: .INDENT 0.0 .INDENT 3.5 from cherrypy.process.plugins import DropPrivileges .br DropPrivileges(cherrypy.engine, umask=022, uid=\'nobody\', gid=\'nogroup\').subscribe() .br .UNINDENT .UNINDENT Note that the values for \fBuid\fP and \fBgid\fP may be either user and group names, or uid and gid integers. Note that you must disable the engine Autoreload plugin, because the way Autoreload works is by \fBexec()\fPing a new instance of the running process, replacing the current instance. Since root privileges are already dropped, the new process instance will fail when it tries to perform a privileged operation such as binding to a low\-numbered TCP port. .SH ENVIRONMENTS These are the built\-in environment configurations: .SS staging .INDENT 0.0 .INDENT 3.5 \'engine.autoreload.on\': False, .br \'checker.on\': False, .br \'tools.log_headers.on\': False, .br \'request.show_tracebacks\': False, .br \'request.show_mismatched_params\': False, .br .UNINDENT .UNINDENT .SS production .INDENT 0.0 .INDENT 3.5 \'engine.autoreload_on\': False, .br \'checker.on\': False, .br \'tools.log_headers.on\': False, .br \'request.show_tracebacks\': False, .br \'request.show_mismatched_params\': False, .br \'log.screen\': False, .br .UNINDENT .UNINDENT .SS embedded .INDENT 0.0 .INDENT 3.5 # For use with CherryPy embedded in another deployment stack, e.g. Apache mod_wsgi. .br \'engine.autoreload_on\': False, .br \'checker.on\': False, .br \'tools.log_headers.on\': False, .br \'request.show_tracebacks\': False, .br \'request.show_mismatched_params\': False, .br \'log.screen\': False, .br \'engine.SIGHUP\': None, .br \'engine.SIGTERM\': None, .br .UNINDENT .UNINDENT .SH BUGS \fBcherryd\fP should probably accept command\-line options \fB\-\-uid\fP, \fB\-\-gid\fP, and \fB\-\-umask\fP, and handle dropping privileges itself. .SH AUTHOR fumanchu .nf cherrypy.dev .fi .SH COPYRIGHT This man page is placed in the public domain .\" Generated by docutils manpage writer on 2009-06-15 15:07. .\" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702477915.0 CherryPy-18.9.0/pyproject.toml0000644252176402575230000000034114536340133017144 0ustar00jaracoprimarygroup[build-system] requires = [ # Essentials "setuptools >= 45", # Plugins "setuptools_scm[toml] >= 3.5", "setuptools_scm_git_archive >= 1.1", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/pytest.ini0000644252176402575230000000556514424762003016276 0ustar00jaracoprimarygroup[pytest] addopts = # FIXME: Enable this once the test suite has no race conditions # `pytest-xdist`: # --numprocesses=auto # `pytest-mon`: # Useful for live testing with `pytest-watch` during development: #--testmon # Show 10 slowest invocations: --durations=10 # A bit of verbosity doesn't hurt: -v # Report all the things == -rxXs: -ra # Show values of the local vars in errors: --showlocals # Autocollect and invoke the doctests from all modules: # https://docs.pytest.org/en/stable/doctest.html --doctest-modules # Dump the test results in junit format: --junitxml=.test-results/pytest/results.xml # `pytest-cov`: # `pytest-cov`, "-p" preloads the module early: -p pytest_cov --no-cov-on-fail --cov=cherrypy --cov-branch --cov-report=term-missing:skip-covered --cov-report=html:.tox/tmp/test-results/pytest/cov/ --cov-report=xml # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here --cov-context=test --cov-config=.coveragerc doctest_optionflags = ALLOW_UNICODE ELLIPSIS filterwarnings = error # pytest>=6.2.0 under Python 3.8: # Ref: https://docs.pytest.org/en/stable/usage.html#unraisable # Ref: https://github.com/pytest-dev/pytest/issues/5299 ignore:Exception ignored in. :pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception ignore:Exception ignored in. <_io.FileIO .closed.>:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception ignore:Use cheroot.test.webtest:DeprecationWarning ignore:This method will be removed in future versions.*:DeprecationWarning ignore:Unable to verify that the server is bound on:UserWarning ignore:Not importing directory .*.tox/py35/lib/python3.5/site-packages/(zc|repoze).* missing __init__:ImportWarning # ref: https://github.com/mhammond/pywin32/issues/1256#issuecomment-527972824 : ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:DeprecationWarning ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:PendingDeprecationWarning # TODO: Replace the use of `cgi`. It seems untrivial. ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning # TODO: Remove once `cheroot.webtest._open_url_once` is fixed to release # TODO: connection properly. ignore:unclosed =8.2.1', 'portend>=2.1.1', 'more_itertools', 'zc.lockfile', 'jaraco.collections', ], extras_require={ 'docs': [ 'sphinx', 'docutils', 'alabaster', 'sphinxcontrib-apidoc>=0.3.0', 'rst.linker>=1.11', 'jaraco.packaging>=3.2', 'setuptools', ], 'json': ['simplejson'], 'routes_dispatcher': ['routes>=2.3.1'], 'ssl': ['pyOpenSSL'], 'testing': [ 'coverage', # inspects tests coverage 'codecov', # sends tests coverage to codecov.io # cherrypy.lib.gctools 'objgraph', 'pytest>=5.3.5', 'pytest-cov', 'pytest-forked', 'pytest-sugar', 'path.py', 'requests_toolbelt', 'pytest-services>=2', 'setuptools', ], # Enables memcached session support via `cherrypy[memcached_session]`: 'memcached_session': ['python-memcached>=1.58'], 'xcgi': ['flup'], # https://docs.cherrypy.dev/en/latest/advanced.html?highlight=windows#windows-console-events ':sys_platform == "win32" and implementation_name == "cpython"' # pywin32 disabled while a build is unavailable. Ref #1920. ' and python_version < "3.10"': [ 'pywin32 >= 227', ], }, setup_requires=[ 'setuptools_scm', ], python_requires='>=3.6', ) __name__ == '__main__' and setuptools.setup(**params) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5514038 CherryPy-18.9.0/tests/0000755252176402575230000000000014536340235015377 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/tests/dist-check.py0000644252176402575230000000330114307416152017762 0ustar00jaracoprimarygroup""" Integration test to check the integrity of the distribution. This file is explicitly not named 'test_' to avoid being collected by pytest, but must instead be invoked explicitly (i.e. pytest tests/dist-check.py). This test cannot be invoked as part of the normal test suite nor can it be included in the normal test suite because it must import cherrypy late (after removing sys.path[0]). """ import os import sys import pytest @pytest.fixture(params=[ 'favicon.ico', 'scaffold/static/made_with_cherrypy_small.png', 'tutorial/tutorial.conf', 'tutorial/custom_error.html', ]) def data_file_path(request): 'generates data file paths expected to be found in the package' return request.param @pytest.fixture(autouse=True, scope='session') def remove_paths_to_checkout(): """Remove paths to ./cherrypy""" to_remove = [ path for path in sys.path if os.path.isdir(path) and os.path.samefile(path, os.path.curdir) ] print('Removing', to_remove) list(map(sys.path.remove, to_remove)) assert 'cherrypy' not in sys.modules def test_data_files_installed(data_file_path): """ Ensure that data file paths are available in the installed package as expected. """ import cherrypy root = os.path.dirname(cherrypy.__file__) fn = os.path.join(root, data_file_path) assert os.path.exists(fn), fn # make sure the file isn't in the local checkout assert not os.path.samefile(fn, os.path.join('cherrypy', data_file_path)) def test_sanity(): """ Test the test to show that it does fail when a file is missing. """ with pytest.raises(Exception): test_data_files_installed('does not exist') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1683219459.0 CherryPy-18.9.0/tox.ini0000644252176402575230000001023114424762003015542 0ustar00jaracoprimarygroup[tox] envlist = python minversion = 3.21.0 [testenv] usedevelop = True commands = pytest {posargs} codecov -f coverage.xml -X gcov passenv = WEBTEST_INTERACTIVE CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* CIRCLECI CIRCLE_* setenv = WEBTEST_INTERACTIVE=false extras = testing routes_dispatcher memcached_session whitelist_externals = mkdir [python-cli-options] byteerrors = -bb bytewarnings = -b # isolate = -I # FIXME: Python 2 shim. Is this equivalent to the above? isolate = -E -s [dists] setenv = PEP517_OUT_DIR = {env:PEP517_OUT_DIR:{toxinidir}{/}dist} [testenv:cheroot-master] deps = git+git://github.com/cherrypy/cheroot.git@master#egg=cheroot [testenv:pre-commit] deps = pre-commit commands = {envpython} -m pre_commit run --show-diff-on-failure {posargs:--all-files} # Print out the advice of how to install pre-commit from this env into Git: -{envpython} -c \ 'cmd = "{envpython} -m pre_commit install"; scr_width = len(cmd) + 10; sep = "=" * scr_width; cmd_str = " $ " + cmd; '\ 'print("\n" + sep + "\nTo install pre-commit hooks into the Git repo, run:\n\n" + cmd_str + "\n\n" + sep + "\n")' [testenv:pre-commit-pep257] deps = pre-commit commands = pre-commit run --config .pre-commit-config-pep257.yaml --all-files {posargs} [testenv:dist-check] # ensure that package artifacts are installed as expected usedevelop = False commands = pytest tests/dist-check.py {posargs} [testenv:setup-check] extras = docs usedevelop = False commands = python -m setup check --metadata --restructuredtext --strict --verbose [testenv:build-docs] basepython = python3 extras = docs testing changedir = docs # FIXME: Add -W option below once this issue is addressed: # https://github.com/jaraco/rst.linker/issues/7 # And once all other warnings are gone. commands = {envpython} -m sphinx \ -a \ -j auto \ -b html \ --color \ -n \ -d "{toxinidir}/build/html_docs_doctree" \ . \ "{toxinidir}/build/html" # Print out the output docs dir and a way to serve html: -{envpython} -c \ 'import pathlib; docs_dir = pathlib.Path(r"{toxinidir}") / "build" / "html"; index_file = docs_dir / "index.html"; '\ 'print("\n" + "=" * 120 + f"\n\nDocumentation available under `file://\{index_file\}`\n\nTo serve docs, use `python3 -m http.server --directory \{docs_dir\} 0`\n\n" + "=" * 120)' [testenv:cleanup-dists] description = Wipe the the dist{/} folder # NOTE: `package_env = none` is needed so it's possible to use `--installpkg` # NOTE: with the main `testenv`. # Ref: https://github.com/tox-dev/tox/issues/2442 package_env = ❌ DUMMY NON-EXISTENT ENV NAME ❌ usedevelop = false skip_install = true deps = setenv = {[dists]setenv} commands_pre = commands = {envpython} \ {[python-cli-options]byteerrors} \ {[python-cli-options]isolate} \ -c \ 'import os, shutil, sys; dists_dir = os.getenv("PEP517_OUT_DIR"); shutil.rmtree(dists_dir, ignore_errors=True); sys.exit(os.path.exists(dists_dir))' [testenv:build-dists] allowlist_externals = env description = Build dists and put them into the `{env:PEP517_OUT_DIR}{/}` folder depends = cleanup-dists platform = darwin|linux # NOTE: The custom command is here to allow resetting the global # NOTE: pip constraints env var. isolated_build = true # NOTE: `package_env = none` is needed so it's possible to use `--installpkg` # NOTE: with the main `testenv`. # Ref: https://github.com/tox-dev/tox/issues/2442 package_env = ❌ DUMMY NON-EXISTENT ENV NAME ❌ # `usedevelop = true` overrides `skip_install` instruction, it's unwanted usedevelop = false skip_install = true deps = -rrequirements{/}tox-build-dists.in passenv = PEP517_BUILD_ARGS setenv = {[dists]setenv} commands_pre = commands = # Starting with build v0.5.0, it builds wheel from sdist # if no format arguments are passed. This makes sure that # wheels are not dependent on the Git repo or anything # external what may be missing from sdist. {envpython} \ {[python-cli-options]byteerrors} \ {[python-cli-options]isolate} \ -m build \ --outdir '{env:PEP517_OUT_DIR}{/}' \ {posargs:{env:PEP517_BUILD_ARGS:}} \ '{toxinidir}' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702477980.5523598 CherryPy-18.9.0/visuals/0000755252176402575230000000000014536340235015723 5ustar00jaracoprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/visuals/cherrypy_logo_big.png0000644252176402575230000004265314307416152022147 0ustar00jaracoprimarygroupPNG  IHDRV fPLTE̺udȡrR8oVĺtkFx힗sɒR۹hJϯ[kb:ϙTL޻džbqfwamXNe嚱]pԨ`ڦЊҚ{xe}yg{tUqn_lhThcK𻇄vYYX j񾮭ggfdxxwNNMBBA '''887//. ɖźc]ErmR뼾箱䠣܉|z<8'~zbSYtykpKQCI;B[adijc\W>3;&#+,4ЍmVHޞkRA7$#"?=0pzX'/m/-JBģ’k-,ؙﵦi鶁鬨G:쯧eˆ묿{,*PL5 ԯwɅ}sJe⚸iݑǹx׋噝Pqeߔpύjz喝ZᒿoЄνq㒖Y|W]IۊZtT߉݃Tȸjk߆xz߃\p̧d݄݁Pd~rϽjLi7BfIDATxb8E#w; xa!mzh{C6ƓoƑ@nbOuxprb|kӴ o+:, ci(ᖦz&1PK+yTu!J\M eP#g>@qoU+veͣ9 erkl "߼76;?U*A6MV"hMz9͒[{h0vm҅69 q|g%~nf>y}Q=T~s:zny˟I}M"g=O%\ @HTd&_j2uPԢziDUY]zKdktvitM:i=y5t,d[5\U:Uh=PӶt{%/ETx['jNxvֳK.բJQYXS.jQO_$;y-*]L?x~o*tP1_]Q7j8N#roEn=XU1 <zL+uvGH:mdĻN2go `{9B*/j0g}j*ȵ=kX5k0WdੋR%d6 ԑD\')eJ 6 tFfZ-aeUpIX8͐1FS+H**:J;DU4@3ubXQ%ib RN+*QLNS7PP>Hzp8EI?>ߨ\-;,NVUꘟxYZǦӋ]Jґ_jf=V lֽX@hY?ÏxBhmƉXŖ;5js(*Z-KUxl|^/9m,zG{ dIa!A68Kl pލ؊qHxO'WYnŌ>⦅mߐ+@EN3 *3FM x(UKM#HoEQKvGaYցUY, *ɵߔ.7 H$QNuJ(KO~촪AJo ,pQ~H_,GNc-w:֭ _\]FE\ 5;p%J"NgBשD~6CN02ʸȄ`ldp``ttt,L"|zW 茮JgxYNȠb|Gl./0 :?,q\:*p#U}kO|l bVOmܞJZ 3dU] W_ЮJ2~%\͊4+ lνBvuVh!uÊt M=V;zW=N"{EUU~IUVQyV +xQ4W0Dԋ`r0 N+VD ^@ KR1Zij`s]Տ?Ԭ*%U)V'F>JSxɻ+x(2 ^/yq ch4"es`$ { [|3hS+0-}v^.WEa eTFD/~AbD' ڛM^>|Qȑ#GV1~"E0`\YT6GPTU1~aiVlv||llt`pp <+d>ed#->qTĬʽW׫U' >V=+!P4X63++s\:N&'( YO6QT)p)Q<9y&ǚbT9V^^Ī̄d]f1nj" N%чa ,z/Y(#K2\vJyu3hP WV\+X!,0#8-f!{̓2d`tJ$u…ən/ZWAU_?9>nc) DBT#hjC{1xWEp:E2%?+)pH)tũdYnMfլu d§UW&bbT& pdb(~V:ujZJeV4d;qXg%/\>Y^C_U.N^5zVgU޹s? O}|B[U8,3`\T;z^ް2 U9Yj6_m=xo0*=0z67b[IQ_Y Y9k:efw?Z~O}),E7_SxCk `6Z䂇RubUp0 ք7L̪ Pj:EREﯭ "Pp)!)!>jljk׮>Jx޸UTj]SX-V?n`l$W[±fXL]&ew +]{sjnsZ[[Ca3U+%]S MUPf#Wk+Y PZ5%&Erk=u+gSYYUiﷻ*sn5L@Pnx^dzkWˊ|De&:;qR)Φ:e@Vzj#(n?tj}BV2cm~ >Z|H F?20oQi딑fES,:rnV*PV|Wó8)jiبp ';`< J %e%4ѳڡ&io h\ςIПq>#eHVӈ6ʥ"wn'~yA`#r}HU! 0Laf{1is5ʕrUnHS@hy> NJX]8WmSՃ( Xgi!=^ԇҁJ EeaUuP5\x&r`c| ESRV[A:(:`uoykrތvj}@}o{<bȃyuzE`0K9b}X#P~>(o7(5,N;#͡5`0 cr(b0Xn/\2tYWIc ʄZ),4hNH}oYLØy"bޜr7s XXmU5GwU+W> 5Or=R 2Q=: և.a% AU 8vjXU>R "BRQ1hi?@eD"g2z'kIrlRƪ#xxLWet+<  W$nn;i}}%SP)o܏ BLSuڕ۽śR+aS j XUE5(*r¸T,\_js?e%cyT>b%Ū-3yy"`wf!>kl"{0c Bt2.Y],S8, #O&KF~UXM̌AR|YU}yȌEJZ0GVė"lbFb~S%[+Z^[`zƞ~wikzT/oC|7n٭ƥfp'VUq8w4zKVV w #Tʙ0b,(D$+OR1vsւu ovb~H!5H?Dxi9DɆd$29,V'F#"$43{pOQ %_ xfWܴw[R"Us+PJ^/]=w{Bj;zD[[Kn9*RG#=VȬc ^:Eq'*L 'H--QZI8,)@wГJ)H!<)J.Ac. "̔|_,08Ff{Q4T#ғ+YEH'([`iz&\ -?p5 3@\$or&3#WCKNd9j%_Gt0CN Vd3#L$Y7q~ OzAW 7n^$hLoy$`+o (U++ᎱJzj*kJVxu /xo?18[kRVix/mf!e#gIǚ-35YǓKΝ q<@ODٮyc RM4pVܘT]Y%X(|'vU:ti%3L6B,R&#QO+UKS/x d0^i`tqj3xM%,PKBZ]~cobWerie}AVϏVnְ*;*5q! Un/v& Vi621,h 0Ǭq|~d)>=/"IX^BUYUU8j(9 U%Srh>dҴ̲˖Z:F[[M DdU8w_\`/5q` ={v/ND$Zo -QI\膕ʘ2+c3U[kN6d`_w>xV}rtzA7ǯiIBקPG}e͇ fŬvݩbmyK ?s$) ۃcLU%GMj<?=j *#hB0&j9C*:IIĦMMJkX;kIQNYgR*gW^!AaNy65*ӓ>AR5̰lz/t3X@!ܪF:{j=NO5]z qnC)4j5 ~n 3*K %pH:ѹz {%rl"SPc O9HQVL8A/I /LU哥Rϙ^Φl:P0J,JݐI;JWҬVxR~uX5׮zj笅'/^魊~QrY o^:RwPVwm52jF0jzm@C1V e_)Eܜ$I'"3ӗ|FTL./.?Զ)F_iV|BYMƵ\L3J>H78b l7h63t{JЄKYͳ@JuG;oZ V<=q~"Bㆦhq}  y$^}t0VcaUQmX l\)$qh=l_F=*^]Ƃ̆{A׺UL΄RA5vM(3_-`ilsl ,6TVjRx/.W{X,h[m}Sb N``y z*GefaT9f]5v+o~&|ˆ ma_m"[Ύ*hj=?}ܮ뚬OqYECK]N4GodiP̾ϤhFD֌y;?/Xu7쾣Ϝ鹿`kk۬@P_YZug;$+!߾xS&_9`gG[k[H$xDj lDvZYWZv9@iNo,%@<% ΖyM}(BUEhg[XռM*Q.U(ԏR7[, NS2E[ >7GuVbV5PcSa6XLy^];E =Y:gR .Ɗ݊DoPH)%!ߠ}=7N^pDT=/j4{s6J PyZrH}[z9jSt@lbQUqXC~ܠET󖫊z,3#e:9"-K0V·`r KWc_jmP* O+L{F ?SsysWud߁cy^oT՚`zÿԙٿ9]]_[GrۛkXwTaD~qq cU,nX:UFDUմ 16ڻ;EX|`!*+4y&Oci4|xZnYRcukmiBՊv|: VPh(55vN] uwt:+Ċ*nU3 / n\/!=BsڍʪK7xQcͭ'6shÌG2Ԩ`Y+e;ݧJdq-8<<>RI 3ա(hi]CW%Qbu}V+V?X!aSX|2:haa췅 Fhj)jnuw'oo7լ^v8HQ]MۄĊ@uq%׺N-4v%Bu}7*`%gD,+ jqtYb՘VW5 ܪ /_Nanͭ[>Q_&h> w;Jwb^ -&{3 BHLb9̪鶭ZvEէ] (W׃:XGK`_ܹ3y*ӓϲ+m3p­Vq׶šT$tj lM`5!aW`&UC`" V&Ta4eWxJ/Yhr};Xr˺}g)6T=펑BirX 1**+VS{z ޱ+'tX*hC{X*?SG/"_D5%YÍ*n+bƤ," /m"ZiK"F#&<΀qy=VPP)L Gs袌1r+a49)5V1+5 ߡ ʂ^;::ڋzwVroi f`wXX!"/K52Bu=Af[F:hk^ƊCԼ1:0+`H$c 7.3S%[fHlXwO3j{3x7Rx[ Gur3 0NJO xڵpSQG̲RNd@sP[ 1+@UѪ#d$8`eH|Ҫj X&jzɡyfUJV, =&xDudVI HbkUCL`(ի6l8!fͲ80dլ*nB<׫w2!6@w2NɝycK q*qyv}xw;ү_| z8$V *^Fr Zop}D[ZO`R (WCGbV7ME\ecS $rvήnֵw*_|jR ImպUʔ[a?YcO-]{x5s+GK-e@sētJ2U&y?$s5Pnv-=U^IbFUVVmmXI:rX-/-P=2Du:kUNvRA`)˹[q>eBVC%2YAu=d͊՝IЛ#Y|SzGx-Xa)m*{5xшScH[gWX^T t=x9NHЭ@RK2D@wf P@@u;Y,JsevN-I-˱6tΊԉ6@ L}XZD2f̼\׃brUh!Ku8G2)[a t#kgi8 2\]gx ?Y 2d!bBPݼ~7X W]iVc9D*յDWb 5DgX;:s}:nj/5\*)e,로J*D=\ OÓ1VL#2lV UeWZqKJ+^u^ɗ\߻^ĕE4j:X%._ݵר wc/5\\r;8ʀ+p/%^9V!FXhTɀrbU&[X Qɖ7+ʮn(e~%#FR:o^d o""_ G;w-|ފTX%WS`?إT:Rc3Q:+j|6O[#T,Lv2ٵzV`VUȮ`LKŠsU*zDrMچl:\5rD8N攭׽6PVB0FdjX/o;à^X-΀`V7`WD ,oW-SIWĊ TczrX',.X3 GӼX2*b9{1 +(ZP)Pn mson危B`Uf5h +k%g@0e(c7Yďv;*2WSZa6jc *WQ,,TE;c`l9jX5va !VXI"-Zg s׷l@ZcBb`@ɀY]))P٭]g1?iWOG+6X Zi*\s W(W)\RZC;8~ NtW Jt nVE S!BCWXVǭa+r\E?ANfwAƬbuA9Yʮ tDWI =#AUHy +$#-RXͅB~& 5-emŭm58X†el椇X' /o:t2)ׇbuCՍ<((ivyX5a,u O)*bE{iKX fZ>إWK4i5+tKxdS+o+*} 從uV'}VsְZ̊|ڞA~hΎ=Ua^jɭ;H)lq>_M+Eܹ'=*T Ȣ;T۾U_RYL,j̝ h   MPVwTXY!bKۆ% !uڔyYՀ \:Wԣ^|@:{<1ymyR*W*B@̫b :xtqOj#mdN*0Ɋ3Zgia/yGzc-*T>JP ߃> S (*D3Uj^EQZ)>]Ay ^jpjs׃7"Auo/Zq]VrLa*t\ߒuPpbo@jjFWm59&(Zռ\ay+:\)K͇hD+"Bi5't9\+ޡ*pvwV d%/k0%ZNZ9n*.xu4ZE10 8w:fCBZi@8wv|21oZrz^%Yo&!{X'OVh4lVp,/axT`:X5VX.{z`#axKEV5:Y1ioHĪqχ|vGg5; pU&Пd%>ԂaWŭ[Ww֎«wSv7Yreh 4Z VpJ\w3*H$ϰio Z{%aZaEDA6п"xjTѠ{&  IN6~2X.7ƫ_0\ p> /AW v {bPXjV [V5֊-]=fC-i%hZ m lHD\+]'jfqUZ>(xVœ]4UvP (>Ԛwvcĭ2[[f2KR=o,匤-1иXx؜A^Z52+a*31}t`bG\|/rQ-_1ywJ^oVV VUZH**t-_91Ľ\"&Njo~.IuW+L ]=ڏZMFڰc8Vk^ޝ͛]]hg$\Z']^XeP- W sr15)" [l*vZB6ܪ! /54 zg%6B c؉gɠF6s6B>8_=cM' IO)fХ;y%W~VWXi-nVju> Z/~0>ӻtvV Q37 EAQ)/ThVY."8^PUQ IY'eP1vySf%.X^oT+=)Xde_vn:!ؑXǮ ВRʯ Q {PYU%Cɭowpw9(B"䕗^_Rb"JBz`ZX ǣ*!sH&W0<C[' Eꬪ"=ey?ɩlbYbǔhמr|I21 LGR!2*27 ]c]"rʋ |T*]K:=V*BJh'N~㯅J$RKPUO җc"ޟ/TBdb[+\쉱h RJREOE0E?Ad!V4+&S9i\ݾO`=ⵤYn .u1T#EDjCðO< ~ǕYހXbzR:2r XUYjjV"&SǴLYQlڂh25]#YoNHݟa?1tNewgSr^?q_ǫnvf? r[7Y$y'>mI!1(]ʬDDDDDDDDDDDDDDDDDDDDDDEq6A!6{soYK(V-/ƮҵEuE6`zCSe FՅ 缓?+[7\ݫܕ䔳@}eINSkke8 ݙf`l&R?6{\,-v&sg:ILj*ôUWf[pGըկ8tVe'*i [ Wn/8'_RqN tA}I&8MҮ7B!.!X$ ɈH_'gm?3R l.L^RŪbojO"bݬj +EA(S8T$Vnfa>, jafRUZ#i(#B`r"fTnpB:+GWj~_c3q7grpwDU>P4z_fa񞒷7MyHo(،4}V-N?`Xb!sv;Xl?H緟Z55jrSI\1s,D'V=~RU&R}X R+hE ztՃ5j֣m?9Яގ1m!KzLq)xJG 'V3nqS#M qp',5a`^Y9p~c{ɡ[ !է8[;1\rP)q䂹ϞPǑ9wCB2MN ǀ&1.+XtPnֳK[TXٙv|Hqk:J At|yhp=ͩ#QkM43vYqcEIJ-RYF!N7ᔰ4:n9#wI<4K[9 WMi?h|+NRU] E޸|r`rI#"W7bul~aX!Ú)$" Gh#.oǠZZW7ץivnV|?'qh>wz_ip^HCcjjYI6G|eЅWN[Y.{~8] Ll928t 寛XB% !]jd+344Ao/ie 4%-ꂼ7Gw€kYYie26Wb#}* |xWeUA!/ǯSf#wK&z JûaMk4KƆO,ndQjY w6,Z9ϧ[i5.z$H-$y$+F%Bgω9 t 7O5 Uv \b{7pIΠ3f3si$#Ϻs"c )>J}䷚=m[`abԜdGk:eg?׫F0 KV=Ʊg/z̸KLy5ëY@T[XJ_ub41 @<]J_5彊*V…k(@RXv~Q^<ՆIϔc_ﯣګgbV|XD6Y>H^[6c񋔯݃yGչ#mZ\t ONgr~9$mGW5q>X==e6x]ͅYaɬΩy7MSzxe][Q3[\ wdzsY6-y{t30E~xxl/"}YVS&>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ '"   %#!      $ (  )  & "& &././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/visuals/made_with_cherrypy_big.png0000644252176402575230000005005514307416152023143 0ustar00jaracoprimarygroupPNG  IHDRV fPLTEɩxgrR8ɢsRźibFlskFɍXۺhJPί[lc:ќT~J޾ȄwnZagqeך]䨴_ة֙믵kЊ㓯oD8"۷sÐWlӮrwᒞP2Ѩf|Х`t|*ߚ܊ܑrևӀچeЂؔއьrx]߉VFﺪm߅{r߇ٵk݅ȇ߃ɹmr߅i³n݃݃ځϽj̐݁Ä~ǹwd߃bfx||箱䠣ɺ{sokp9@&%-di—|tRqlUkfS뼾|}|zYYX CIKQhhgOONCCB%%%.../6܈ɷg\|xge]=VP{rId^W=EȀ{C:887;7&uqpV}`r긊˔SYty[ahbzSM4r$#"@=0k^P m/,KCw Ǣz㚖Y-* TWC֐dPdLL㏆JLIDATxv:Fߩ*/QUpBh et氅G4zwqx op#S'}Svv`L\ 6b 26dfV `7-b01 `aZC3 `=2&\\ f5(UlZl  6FۛFXh+0~zv߼zHA,SRiؚ~vLvV(6y/o~泌Ұ72>V:{Gԛ̨8yog{,6Ki  v ŻNqs2˩=*/X @HTv/{TJT'_6Uwv(騲4U;h-J"UX_Ǫ @T*[Y8&WSU-3_/Q {u;ոd[V.*ձQ&9]NY'(U6Gih5U("aZPi[1k٭P$_fZpF: !ƘppKX QւAߥիk]PQ)X 0/ϿeUf 9Vδ~!lǟ?Il[uaKq'!,"D,q'yHF ꄚ1$B%#8-BR@HEQPtQ}SuY_kL_EXȳWe+ BSġ*"IVM6te^+F7$zWEbFU{gDHSohd0F˸Yu;짾QὨXm<Oa3ELVeZn݇8śRjY]aa-M;9U |2ud٥&K[N:ڐ$ˊ4 3)wraLB+=ʫU5m7+4a&DTQ9SzuD *n\\K&+kV&+gu:# :<u~Ej5i_{euaRC˖ʪ6&-^.ȒiUޕ`Nj˶UuTe-Wucz-[ƸB,+ڨo,$E(̑$8WYU>BTV>1ɲJDAR ( # &<3J W5-+bZ&+W= J_?.Vz~Θbe!3U,+@t\ݺ /Vo`b&'@0d%ʉj J~quR'`jתnl;d5bV Yz0!Ȫ_Uժ[4Ն&<7?>ދ)nǸɴΌ"j1&;9:Ӆ5%ņQ˹O͇]>龺 3qBZp:$(M}v_oV+3߃ZvD(# 3Q N#Nq$iT|?b:Ɖ井O u،VaĽmV3Ymf:VWĚc@,/BMsqGP[0r:lABBK75i‡'F Qvv Id!A]vZ{n譭V8fF0Iw,`[Őf6 ?V3Ux*Nn]-^$8P/ێk1]U+ĝuɪwz>nXő4 wb%$(nlZq͐:: ֑³BRZyhVc6sL[ݽr Uޒެ@o5_VFR[9E%`ggL3M헫/\akIm6+z% 8VT2}BJ"ض~ >S;^ JqڐHg߇ ր=TYSVGgU&RMKZ14DYXڙYrn#VzdZ7Fak}+֕VKNc`#]Z)qN"$bOxzL^|bc4w T/C#gSl+'\}wj{T` ,VC& u+IJt+K|FI'̧>֭M)ZmN׀BT`iCU+ڜVlגPXv؈oL~lʎE_[k? ϬV3Bx㯲cW[+l,ep8&;)Zuss%l*q*jikՊ)?JAk5ʟk-`րns IP6Ն4 i2cF`ks/TbJ#fh5dt_U_CaKqsR jgUS6[6_TZ\?Z&Wvr#XU_W~WM+)r6ϔt>}F',iL gX N>|lK$/LCH'M 2nSք/VXV׆@_.jR,Ra-p=d$_wy(oD+C~zR,m a'ʅ. ~h+iO>]bS>յ'*` 3zt~U"Mi^6 G 8R(\XΒu,By(R/dt+CL-nr<5Y}6tTߚUO+ϴ+O3/MJ-$_b Q 3R&[) l%F*juﭷqe]c Ԣ/Um}2{\e.\IcjWaZ j]gV[kh+Yܛ8R8p]z:+78PMQHpK jެ9jWe)60-F+*j-@($U}B+Q߷Qu| cNBxʽ˟׈VqެԮNSk6gBZ,(R qE. B4q7`DH&E rZq PX}ͪFISVJ+u]Fğ]qJC4S)1Jdx _7*';}B8r#N єUlτX+,dpEw 䖪1dQ^J+ $➲VwLW?>uTghf KRw8?fx.ABWob؍{t!\JgSn~={:ZcZ'۶Q2嵒)@6(,3E.$wtw|fHZɣ"?DzcZa3@QrʐóB;<שx8%ٿ%B/;̯rR/dG3 ոzW ɹ4D?JM>1ZVV]4eOΎWgLH!u T,!tñpXYeK9.,0F=wT^Je_*jиw/7,iusSe>`K^,&rzݾM3DW 땋89l6 1 T;[ܷ{ό3C&D߽7~{ \OpbgU]cTsX4<ՃK7TQV$bR;UUnӾY;t.w9"t%^} jlH=roYv&hI~ ^UF6kizXZG/ub"b9WK1YޫJP ϪrMtmA dq'cߢmo0+u`mkiimm V+JLdUv]VF z:ݑ#GӤTK˪L] '_Tx$ SvJĕtuQe/#~ Pxj1lbH*۬!}ŬO"]A\YjY,g]C] Ow9 }Z8r5n[~z'7bUsG(ԩiRS[+82YIlme3z5C/^0܅`v/փ.]|֜Y=)Lf 66TLutBݑ#h =iCW^1kgR0gۦ? XLx ]s?g=*еk_3AX)s7n3AN[,{Ti\;2HEvm[~ UoU T07 >3EZYĊ iJ㨀s]݀wdefVSz+@X43V$RjRVAYYrWEBu0\U4p;):=bR)bQGԙE@X$:AWͪŇRP'eNRVorS-`Pq*TD&ͬlDV:`NQ|~dfu0WQ Cz$g:aX=FNk6T*иLN.]]:ݒyw>,Q͚wv0Ҭzfե~RRdzj b^FvqUӪ.Bz!WR(PG"phQGmO:] x f/#Y^!YЬ&J/_&W$MjU\-XrQYT%`=BՀ`q\q"WӪZY p3TLxL>TF&b"+aXٍĮ r 9GW2없o+ UyW?ϙbjX|>RtI+B31V:b1TASr5Iƒ"q%֬X(b񊬴`E ߱\MU`, ʪϏBK@ AXZ^wɕ9eEkU%UfJ&cY `GgXP^Qۀ+ک'jQo JVH@u ]%U21W+s) jexfu#,!ƯJ+J&8j896`,<$$hnaE.hW $fU9Jp*Q=vUoː 1F&&wduⱵb+wfmY]%bɝ3iEdR% TeDVYdLw{b\M~G _Vh *FqYs`J7Vd *~wd7+ʬX1] 4xjp ᰪeTq\.^js@UK'SU 8oVbU6qdMF~Gj UoTVZYĈY}ԝl"ɚ%ŒVؾ \_@^,cNuIh}aP} ˇ 4in;l$3;u^{.hnqoWԯ_}]J{UVR|7V͒L6tAfۇs" KBšZ0U99P(0}Ƌ䇡Ee DX>)#RXX cJru*"ӑVUo$鉕PP\2"vezBGl4t)4ztIhUkhC_?#+~%0},`ىVa3\ G@ZbW ]OUtEl~עz1VL,`Ўj$npCSF*U& He;{gB96L XZm]"TKOXMk IcPưsVrRp|wyWek60aiX}ua%{6 MJ!`ekXyg­hյv)'6 7)D(I&(fBH_h=@i*J[[ՈPz6BbLX,@ 72'z4PX=ZqqllrN YVחVnS~pB6EkfㄫX[dHID᠉Gr<גk/T+ϧt:ID4-MJ!K8f4_ gUMa5#]5ɔfm-;A_oT$5(j,AAQPʴ7V?;w A]8,uI1fZ^u*.;Aauwiků̩2 B)" 4. v1VHZ f"K= cL0(XAL~TȖ+;lXCp^´JB.r!*Ke[{`Qbu#4Of$Xjʼ}`l k_ ,?'>L̸e~VBɑ .,Vh|Оo[^pVL02T2J+V"#b&t^+V,j{\(m4 :w鵣k-7Bg֧8D`r1H;Z ?!"i]GQhQl(k@\dr.f'e/GAn]-hCFPxm-݄׹9E>B^i*X }ﺚV\*.UJXFUauP rzUf !2"ktB)p2QʔHÊҞF4 ꈢ4G׺,(/􉺽A^;Pt)Xf)Do+ce2j=B9d 9Zݚ@DXZ7v..@qP߸~k}ogX1jZtJ @ "]Tc["Om8 0Sb*zSl-@&ҒPaFeİJ1+;p [dq;r"nV};)ѧa0z 5!dD'r REغVʷ{gGz r%C?ng/ZNyX ?bO Pe}(pǩ+Q, X1FS`שskiYH*V6̋SK!d V[lꡘ,HG5zK?,[-tz;*?Y'f5iLVJcGZ/裁>HCaV.i|3aϤVA}bCXw|<7 aEXb9x0X<;e ~<}7VBܰIw/NR_)Z/&NeC~/uƲtUh0 L@pQP'?XϮ 0=xјl bI&A"?e2387Zٜ]XmAfX 9T`~oʰWp8X0*/!8Wca͒lgX$a&Wk_OTE3:#]Jpnfi\!Bc1Kk ?)'M?f(5jRS0X\Q xV~jw8 7oD|wcX'#hiOz<7XJuuuX-Kc_xESd 2!V )k]V\?DvW/zT ;շ {[3|9dhr26w*Ž=DŽ>^k&i[QqtWw rcon. D\)g;xRawX61N7 s/O4:%V+UW[?3Za9vJR߿gY߷SX]ꋪX]>VqsX5a u<=n)5fcO5q!ն*!3bPlV"-ɕ8Ǯ H=d 7F޻$<&LZYE˳auCտSS$X_r*?㺭ә_ݮ?(D)fX!E~Hf}3N0LC՟ZMiK]Xnzq?>ǎI)_)gnH>6ܓV*~ꦪ +lVG@ElL !?G96+16 ]"7~Ld4w`jy) AT4"qRaZx]r )X;ժʡ굚E(x{#CT-)mӸۆ``qdLaP~]^cu١y1V 9/yuW; /V;q#+}tn;="_ [/˄9 %Pl+uoK)ӊ+8aWGsN 'c-iG=.BZulz^Y oH',&MXa, 0`J+W#{{ecX X!N/"5> V[vz'VZ:<(kRUh:<0OH%c@^KDhZ(]5S]*0ۧ d31"a<=V0[skT!{_ϊ;|"^Wm]X7ru+_ᡎe&GΗo^#51Vo|Rq0WF%cܠvՄAz',ƅZ:+U7psd>D- + E"jr3bXAOδbU"!oY^ [T}s=.e[,FW؎}udPDh"b Ilǯaz+\Xl',@AեAp1T-}}lT}z/_ #9.V¬"zc}/Yd#>vuf0ª וjyqFܡ SP=~l rO+>vxuTz;mfp~JVC?"#XcL`ğsg-:o- %:{^Q+(E +#H8lM͝vnML P{!'6+Z=lzPaxTޏ{߻. p4VO ;Nh7C,/~ۜױ?Z洨!)ϗU4LœqgE}C8I`H2IN݅HN ZzS=Ku”01uU Q!X`L` TUX,njZ +:c_X`:h-1>*y}q⢤yV]e MmrJihFZbةh%C٩~;i V\jʯE#*9 TۺLM N`W_,B*V?RSM>꭭tsY0v6K]PacA[AM;gʴ2Iس[EƖX| -j(Twy1+)U4ue:\ɟaѧT+*Z}Ev NQ6PdP\N`Ze,ց٤Հ?K ̪ZeZ%yj+j)Q\WB"o" 0 )=_BjoWSW,P]Rkǒ@o6P,)P(29mLX}.œ  q0<>%G*}«E<`>#*Eh߃)瀫ѠU)Z+j:i =QMe{"M`t ]b-[סU2QVv(2=CͿwz{|{&SS[R֮<@V̢ɜĩT+3|+U$xgwUnz+iQj8)XactZ}:mj/ⱽy2ik0u%Ζs@pgwv(LK{ S]StJe SeU@Vb>/`E|VB+hoø++xXjl9DT*iŨ-V ~b=:7[:˦kV䡃K!c{<(AeMӊ%7JO!`9`[Uͬ x`ەB!,VmfkBmYO+1U4qIe:0A{ԪƮ+>Y<\F閌VUV,WYUyOHdN甆Տ3J?5z)FqWK7TZic1L+7Jkλ;zLf բXR+2fȠ!0y7{LhOY rBu]ozE1^h\pV L!W[WXLjT%/mIZmFo .RkS+Ǡ'.,̱'ep2(<ްVts jhߨ)8\bqz*FM[k b jjRz8%(-FVNG+HMW, 7YJZmjR8 @ajfqsuVD WtvVhFJ&aZ+g`)[MV[Y`9.]7yd Z=D*'磁Gek"&V>ym" a .#_/~kJ| bi > r{<:x.W-"ZT=(! DϴB?;MtՇXS C+_P lAcghltTvKSV:8'QкK9`^ dzjէ ƲgPХбcaVQӻ煦,|t8+զI 8*t^#\ǧ:<ZAphpws`9`[Zc+էpV x->Y:K0m% ;o_G2C-QipՄ7VV::ŧ2J+Iy-|pAɧ4Zi5Č^Ʋ@':ހd'(?3zN ^n^C+p YZyr.]q4lVaSc7gJuO i5Šz̧ 9 ]p;_,'W_-kʩ; ,)lʹTGM V>ڪqq0<\!2 :Z8YqyI`mX Z)AKcS]~e9äVAp3/xk/\Oh?9iuXSce+&(+ 6OM!۸VV8hAY!)JӀV3Z ƻ kAyOOcZx=_-/4](XtZ >K%4eGH~W3 ?핁3̱)dx2t%a%mG*GeN⓮P R`mL"&1 y*r6J鄊F$t6KBKaLpyւ&UϫkVҽ:=[B f/CnW`!?܋p!GH'/N?D$χ^Xnt47/N_g26-MV5 |3qTmJu˄ B< I:uË#\eJkC3@8_*!|`+VG@j&ʋcyj`Vá t9`OcZ~^qWX]Dwqh4{Egb*Rl1\QЪ jYihsvhV 5ZdTkT&tV6@ŽգUB!s ̺U\~5r n_l0)?؈_/J_* ^bDbML)}s }7DnPNDkeSV[ _0xE,}!]7\[595dIvF~VVHY`V}ZCz߁pU ?ie'XXX:Ke~4v-1_+6M!Ѕb7&cftR' |(G+ ^zu1 _ QʷjnT:UA!'$C5WZsn`襁Fj28h51Uĵqd">;UӇ'EhȯQɛ)ɫvR{u"Z?IzoI+CZ+4īTWЊv%-Ktpn&(_+*mKM*+Z>ܬ֪3q^rTUV1-_*Vjfw9t[vH^Pߪ|':#HX\r^k Uƶ&߬t,1iVav; +&c6 qh ˬyI?붅 h_ŵZXM<'Vo4DF\hdϫ^F:jV=%v!೭hrZ):V#q}~nvJ:#MU"kK^Պ Kk\(`Wo݃7Wbh Fh@V:|J]~3߉l; Vxz6s=aI B+|u 8{HuAS qBM q)Râ&,ne=57o~wJsQcK >d4BUchJܮ^UmTd4ZEdKm FSMo#׌\D n<;Q*|R٫?Uo;=XYS>XI`q~huʘ Z]WTZ[C=`ПݳƉ=.r38f2tuػ{wWgkA g[I"4f/{ɷzLv%.v3ֺ. |iB=^z ORetP^1h*mgU(&_aOȚ52X;V$ JoMtMX$ {`XVAuP1bz(2$ ϕZ݃Ja +u";=כwKUF7oFu9in<Bо uΪJ !2iWG{Z{#;4;(5>0F.D@Ȼ<5 hyˠ+ b 77`2 6TDFGlPs]~wsgGZ|٭FȹJp_Á7//oI jm*3g$GvK,^ 2 x8XL8 JizZg޲*EΑ#kgv WXicAmZ/x@|Zjs2=@٪|YJYx$׀`UUim./J5T+Ry٠T8V蟍6wXtnrR%H2O`bU4 [TFs Z, 9>S x~[wU:>M*.=2|;, ,]ʖɔ;7Xe0ݞ_0ur JJ;)vN'FYT-=JUw0Zä}(bXF.Mo|)%Y*_lr9aWP2 ҋ]5Y`WT=[\L#-g6,OLFRRLj,-o0\J݊5euK^%2nxCuDЭH,ʳryoP)(B\YjIfIj[BBn4r(Vbח'*ۍ/t4|}kIX qapJO p^W J%hj*/.VpFjl6G!^B~pt3 \3lR\Yr wkn\Hyh T ]Cj`SdfaGJU}p"2Puڈ9,I..p0+nSYU TY5\;r?e>W~zH<5F E .:n.iWUVI :E)ɪ#= 2@ 'C||Dhjͬt9z*G]Q@5L(0ivRGy*mDP(:AaVY2mJp:8zY7PtVMR;`ZmPԳ QP(j*oġ~zJTBݶNjdġ:^tX/zmmOJ.gHȡ/P([J Ǝ  yIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662917738.0 CherryPy-18.9.0/visuals/made_with_cherrypy_small.png0000644252176402575230000001431314307416152023507 0ustar00jaracoprimarygroupPNG  IHDR3fIDATx|XۻEݭk'Jt34HwҠtwIt C?3<>~s_gdd-, N}AOT.R]9=& EF΃rԯoc4MHke1`GMCBhIG4%%Ã8JY9^]MqŧӇdZBKN|1سͭ\L= ccZZ}APsЙ\뮞&II`i GꪎJ7p <'H/e02UI] )`޿R‘_G>4 *о!=/VT UzKf,h@t h`D9-eȇ?Y‘#0}m)J0&ɵ#3 ueU9`X )έAY\;OS8ON&ĪORťY__s@ݟ&peM3ťVpcpoҔ'D -p:5^^'&vqivUH"ʡ:|^w GFCݹAC< D#{ u0:Mrx҄ 3:uW50_`vQmFQFjwqScN{[y%~5A!@¦W͈ )JX]m (|JcaXd Ƽmx3^^fp2lR̯TYZfWH1R(e"}k9_s6Vlu6sCuzd5 YVw-"!^^j=Ur#cbw.@rwm0V+Μ ##CO¡ធp:YW44E 08wM=ՖlvIe$w}k,`[m]cΔQerlCݾb5^V8=a+͛SZUar&~H1q 4|,>,!2`Gdbm%0cK5$G~/݆kuFLެ^$'  1؞zmphbH=QsWgy'lZ3tn."(2s7FY3Q[x*C(OA^D^Bl$7q`K-n1o,pCt+{לrT On5Yj*֛u:fJ$`WaV>?KXUh*Q=X`NnGåa!v+ɗ[1H&!V<(/. X<y]KbkueDn'68WL;C@+))Z.jHH1\qΏ# ΎՊ1VSdE1Ž= 5ᗗ4i~2Q6(A=Xg OѰtQL㣄 6RoTB4 )‚,˴V]RB7wx_KbgMibmzJBjC*Y,V-y ~ 1w6Wd@1~7DhU]h'@Әi b˴A]@Som} e΁h]G@*ر0r㈂ETsy]&b#gsjW;`aI5~Z U쯲4pz_(R?4.RL[)b{bE!ZR1~)ΜPVB5Fc=K$ ys,}¯0޷O\؇QMDK=em5A3^9?JՉ 2݊E? AH1*d^R| >7.Wg>)4%)̰Py^1'cERl;^TIrcI=o]k2N-y#| HU.z} @za#Jj:dT+c4W '6w5 4ܔv1g??ΩgN|na6o)LOhi< Chz5̂lwl4.,쩩WŭLGuX/x<&j.US,'[>I4O0KГI2M+mFkN93>WJ&2uZl<6\_-nnٳg ~7h:ڽ={vJR+VΝ;wѢ)2߿/wTRRfyt Hj]=pR Z RZ9;T?a__NaW[iΩ:fD8C6v;}\$ܲy#OR;<17|MX!6#g\z/VXA Tụ:-.Yu/]8tI*.fЙG/[v[gwlo)5s ==}͛saC r`4HlL<36n >}tt-|.W P墜vYƁ\jYB56^KbdЏ`_(:*lzFQ0>ѷz\=d` e=Pe"/AsF$zh39 S]lc{BrP Yv,C#-]2gnͪ%nshƋ6JU>z:wo =dT+- :$>LmR;5šZ׌;[c^`"onY6J bHJ37nΥE N{8s6uTXh$Z9brq3-23}QɎ:Oޝ࿂?Na4V &0ŀՂܵ)1oc9HGNULkz^0f]́Gz _Q=K r AM,!7kIث ᆢ lBWhTqW}fDDx3dxhTc@1Cz|s7쁐okG<.6ҋ-3alJܒqk`^~ϵB$InH?@7Nf 'O-Sd[Y DŽ\bLx5@<[zGJkQs <9uUPdvoc yڳS;ӲeKYd1\3+&XmcTsw@B=uW[S ;cq* m6-%N͟E.s#W9/SbLug+{&aDmƃ+ i6M.6j(a[v.15`Aڽ:otf ![*QA2o16Pd6'DsKg;(j<> 9N|.ٲ4-t$;J];h.@kX]<{TB㛓slTB{E(@-ZD8-[6I\?S8F0N9xGtك1O&Ïy%˺p1Q&'iZA¡uS~k鮶+& C f 0kV}S]fJ4?|@ Kܹs>JhEk -s6 m(qQ dc"6a.Q (p{vz30F `}0]TR]~Z19:-v9pDSpd2$gȎI87|+8wrs]d[nKHlfTU/gvd>Y6ى#g uR,įdュ G^ꢈ@wsZ_a!- Bޔ(6e wP]$*wq闱qSk.k,]4)DBjDӳ7p.upblC%hc\ [fg*ƒD2l~DU (v ;ɦ3" "=F%tǒdr.Y\u:o.:OMbBO }dХ{Wq5N65IyIpiM,zmm\7Ք#rsîtRLXGpJދkJvd 15:*&n%c 2w6E(jw8~to+)pw}L&K6љ2<]v SSi"kڛ_pD _ eMH7؆߿~d7^`;J0uwX'IDK8¿?7cQۄWs}9u\\swlKcf]H3 5*ul\ =o$lܻBWؼ8pVv5^m⹒/t|ۻihԷOrEEeښ lK߼%r3jYSaɷ9  YjadgP6m^굟 d{^nNAB;AߓqmGϽ˗YHN5j%9]:2pBYBjjgGЈS_73Q&Ijm?v$|,< Sd6m- b`ts?bA$s+csB`r.ie)Օi>}tdO?yNOwj2(dK= شhj؊8?OD{*U)ߩPK1om|vks$HSf(;II'^$A-V'P%$NhPQeaeJ|(|q.8Ō9~$ޖHqta,( 33<<`o39븊W/㌌ ߾00ppZgZgZ? i1 [7IENDB`