././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/0000775000175000017500000000000000000000000014631 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/.coveragerc0000664000175000017500000000017100000000000016751 0ustar00zuulzuul00000000000000[run] branch = True source = oslo_middleware omit = oslo_middleware/tests/* [report] ignore_errors = True precision = 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/.mailmap0000664000175000017500000000013000000000000016244 0ustar00zuulzuul00000000000000# Format is: # # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/.pre-commit-config.yaml0000664000175000017500000000252300000000000021114 0ustar00zuulzuul00000000000000# We from the Oslo project decided to pin repos based on the # commit hash instead of the version tag to prevend arbitrary # code from running in developer's machines. To update to a # newer version, run `pre-commit autoupdate` and then replace # the newer versions with their commit hash. default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 9136088a246768144165fcc3ecc3d31bb686920a # v3.3.0 hooks: - id: trailing-whitespace # Replaces or checks mixed line ending - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' # Forbid files which have a UTF-8 byte-order marker - id: check-byte-order-marker # Checks that non-binary executables have a proper shebang - id: check-executables-have-shebangs # Check for files that contain merge conflict strings. - id: check-merge-conflict # Check for debugger imports and py37+ breakpoint() # calls in python source - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ - repo: local hooks: - id: flake8 name: flake8 additional_dependencies: - hacking>=6.1.0,<6.2.0 language: python entry: flake8 files: '^.*\.py$' exclude: '^(doc|releasenotes|tools)/.*$' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/.stestr.conf0000664000175000017500000000007000000000000017077 0ustar00zuulzuul00000000000000[DEFAULT] test_path=./oslo_middleware/tests top_path=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/.zuul.yaml0000664000175000017500000000033300000000000016571 0ustar00zuulzuul00000000000000- project: templates: - check-requirements - lib-forward-testing-python3 - openstack-python3-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/AUTHORS0000664000175000017500000001004600000000000015702 0ustar00zuulzuul00000000000000Abhijeet Malawade Adam Kijak Aditi Raveesh Akihiro Motoki Akihiro Motoki Andreas Jaeger Andreas Jaeger Ann Kamyshnikova Anusree Ben Nemec Brant Knudson Cedric Brandily Chang Bo Guo ChangBo Guo(gcb) Chaozhe.Chen Chris Buccella Christian Berendt Chuck Short Chuck Short Corey Bryant Craige McWhirter Cyril Roelandt Cyril Roelandt Dan Prince Daniel Bengtsson Davanum Srinivas (dims) Davanum Srinivas Davanum Srinivas Dina Belova Dong Ma Doug Hellmann Doug Hellmann Eric Brown Flavio Percoco Gabriel Adrian Samfira Ghanshyam Ghanshyam Mann Gordon Chung Guang Yee Haifeng.Yan Henry Gessau Hervé Beraud Ihar Hrachyshka Jake Yip Jamie Lennox Javeme Jay Pipes Jeremy Stanley JiaJunsu Joe Gordon Johan Guldmyr Joshua Harlow Joshua Harlow Joshua Harlow Juan Antonio Osorio Robles Julian Edwards Julien Danjou Kamil Rykowski Kenneth Giusti Kirill Bespalov Maciej Szankin Mark McLoughlin Matt Fischer Mehdi Abaakouk Mehdi Abaakouk Michael Krotscheck Moisés Guimarães de Medeiros Monty Taylor Noorul Islam K M OpenStack Release Bot Pierre Riteau Ronald Bradford Samuel de Medeiros Queiroz Sean Dague Sean McGinnis Sergey Kraynev Takashi Kajinami Takashi Kajinami Thomas Goirand Thomas Goirand Timur Sufiev Tom Cocozzello Tony Breeds Victor Sergeyev Victor Stinner Vu Cong Tuan YuehuiLei Yujun Zhang Zhihai Song ZhongShengping Zhongyue Luo caoyuan chenghuiyu dengzhaosen gengchc2 gord chung gordon chung howardlee jacky06 lioplhp melissaml pengyuesheng ramishra ricolin sonu.kumar venkatamahesh wu.shiming xuanyandong yan.haifeng zhangboye zhangguoqing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/CONTRIBUTING.rst0000664000175000017500000000135200000000000017273 0ustar00zuulzuul00000000000000If you would like to contribute to the development of oslo's libraries, first you must take a look to this page: https://specs.openstack.org/openstack/oslo-specs/specs/policy/contributing.html If you would like to contribute to the development of OpenStack, you must follow the steps in this page: https://docs.openstack.org/infra/manual/developers.html Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/oslo.middleware ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/ChangeLog0000664000175000017500000004333400000000000016412 0ustar00zuulzuul00000000000000CHANGES ======= 6.1.0 ----- * reno: Update master for unmaintained/yoga * Bump hacking * Update python classifier in setup.cfg 6.0.0 ----- * healthcheck: Ignore proxied requests * Remove deprecated SSLMiddleware * Remove oslo namespace package (again) * Imported Translations from Zanata * Update master for stable/2023.2 5.2.0 ----- * Use upper-constraint in doc generation * Imported Translations from Zanata * fix spelling * Revert "Moves supported python runtimes from version 3.8 to 3.10" * Moves supported python runtimes from version 3.8 to 3.10 * Bump bandit and make oslo.middleware compatible with latest rules * Update master for stable/2023.1 5.1.1 ----- * Fix issues related to tox4 5.1.0 ----- * Add Python3 antelope unit tests * Update master for stable/zed 5.0.0 ----- * Drop python3.6/3.7 support in testing runtime * Fix formatting of release list * Add Python3 zed unit tests * Update master for stable/yoga 4.5.1 ----- * Update python testing classifier 4.5.0 ----- * Add paste.filter\_factory entrypoint for basic auth middleware * Add Python3 yoga unit tests * Update master for stable/xena * Add oslo.config.opts entrypoint for basic auth middleware * healthcheck: Limit source IP range 4.4.0 ----- * Add new basic auth middleware 4.3.0 ----- * Upgrade the pre-commit-hooks version * setup.cfg: Replace dashes with underscores * Warning about /healthcheck * Add Python3 xena unit tests * Update master for stable/wallaby * Fix requirements issues * Fix requirements issues * remove unicode from code 4.2.0 ----- * Use TOX\_CONSTRAINTS\_FILE * Use py3 as the default runtime for tox * ignore reno generated artifacts * Adding pre-commit * Add Python3 wallaby unit tests * Update master for stable/victoria 4.1.1 ----- * Bump bandit version * Fix pygments style 4.1.0 ----- * Stop to use the \_\_future\_\_ module * Fix hacking min version to 3.0.1 * Switch to newer openstackdocstheme and reno versions * Remove the unused coding style modules * Remove translation sections from setup.cfg * Drop use of six * Align contributing doc with oslo's policy * Bump default tox env from py37 to py38 * Add py38 package metadata * Add release notes links to doc index * Add Python3 victoria unit tests * Update master for stable/ussuri * Cleanup py27 support 4.0.2 ----- * Use unittest.mock instead of third party mock 4.0.1 ----- * remove outdated header * Remove universal wheel configuration * reword releasenote for py27 support dropping 4.0.0 ----- * [ussuri][goal] Drop python 2.7 support and testing * tox: Trivial cleanup * tox: Keeping going with docs * Switch to Ussuri jobs * Update the constraints url * Update master for stable/train 3.38.1 ------ * More details when logging 413 Entity Too Large * Add Python 3 Train unit tests * Bump the openstackdocstheme extension to 1.20 * Blacklist sphinx 2.1.0 (autodoc bug) * Cap Bandit below 1.6.0 and update Sphinx requirement * Replace git.openstack.org URLs with opendev.org URLs 3.38.0 ------ * OpenDev Migration Patch * Dropping the py35 testing * Fix invalid escape sequence warnings * Do not use the deprecated best\_match() method * Update hacking version * Update master for stable/stein 3.37.1 ------ * add python 3.7 unit test job * Use template for lower-constraints * Update mailinglist from dev to discuss 3.37.0 ------ * Clean up .gitignore references to personal tools * Document security considerations for detailed healthcheck * Remove moxstubout usage * add lib-forward-testing-python3 test job * add python 3.6 unit test job * import zuul job settings from project-config * Update reno for stable/rocky 3.36.0 ------ * Switch to stestr * Add release notes link to README * fix tox python3 overrides * Remove stale pip-missing-reqs tox test * Trivial: Update pypi url to new url * set default python to python3 * add lower-constraints job * pypy is not checked at gate * Updated from global requirements * Follow the new PTI for document build 3.35.0 ------ * Imported Translations from Zanata * Imported Translations from Zanata * Update reno for stable/queens * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements 3.34.0 ------ * Avoid tox\_install.sh for constraints support * Updated from global requirements * add bandit to pep8 job 3.33.0 ------ * Updated from global requirements * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata 3.32.1 ------ 3.32.0 ------ * Updated from global requirements * Updated from global requirements * Remove Class HTTPProxyToWSGIMiddleware 3.31.0 ------ * Imported Translations from Zanata * Invalid parsing of Forwarded header fixed * Remove method set\_latent * Updated from global requirements * Updated from global requirements * Fix exception in json response under py35 * Update reno for stable/pike * Updated from global requirements * doc: Specify openstackdocs theme 3.30.0 ------ * Update URLs in documents according to document migration 3.29.2 ------ * update the docs url in the readme 3.29.1 ------ * switch from oslosphinx to openstackdocstheme * Replace deprecated inspect function in Python 3 * import admin guide content from openstack-manuals * rearrange content to fit the new standard layout * Remove deprecated oslo.subdomain from CORS opts 3.29.0 ------ * Updated from global requirements * Remove pbr warnerrors in favor of sphinx check 3.28.0 ------ * Updated from global requirements * Updated from global requirements * Remove usage of parameter enforce\_type * Updated from global requirements 3.27.0 ------ * Set global\_request\_id if passed in as X-OpenStack-Request-ID * Allow compatibility headers for request\_id * Updated from global requirements * Updated from global requirements * Updated from global requirements 3.26.0 ------ * Updated from global requirements * Optimize the link address * Fix test\_request\_too\_large\_no\_content\_length failure 3.25.0 ------ * Remove log translations * Updated from global requirements 3.24.0 ------ * Updated from global requirements * [Fix gate]Update test requirement * Updated from global requirements * pbr.version.VersionInfo needs package name (oslo.xyz and not oslo\_xyz) * Update reno for stable/ocata 3.23.1 ------ * Filter token data out of catch\_errors middleware 3.23.0 ------ * remove superfluous random stuff * Updated from global requirements * Remove references to Python 3.4 * Add new middleware to send API data to statsd * Add Constraints support * Replace six.iteritems() with .items() * Replaces uuid.uuid4 with uuidutils.generate\_uuid() 3.22.0 ------ * Transform healthcheck from filter to an application * Filter X-Auth-Token in catch\_errors 3.21.0 ------ * Suggest to use egg based filter\_factory rather than fragile full path * Document how to use detailed mode in healthcheck * Remove self-referential link in docs * Add Python 3.5 classifier and venv * Enable release notes translation * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata * Allow healthcheck to be configured by oslo.config * Updated from global requirements * Replace 'assertEqual(None, ...)' with 'assertIsNone(...)' * TrivialFix: Remove default=None when set value in Config 3.20.0 ------ * Limit ssl deprecation warning to external importers * Changed the home-page link * Updated from global requirements * Updated from global requirements * Updated from global requirements * make sure we handle the forwarded for headers 3.19.0 ------ * Show more healthcheck examples * Remove pot files * Fix inline docstring to use default path (not /status) * Updated from global requirements 3.18.0 ------ * Updated from global requirements * Fixed typo in SSL 3.17.0 ------ * Updated from global requirements * Replace deprecated LOG.warn with LOG.warning 3.16.0 ------ * Updated from global requirements * Fix parameters of assertEqual are misplaced * Updated from global requirements * Fix unit tests on Windows 3.15.0 ------ * Updated from global requirements * Updated from global requirements * Add reno for release notes management * Updated from global requirements 3.14.0 ------ * Updated from global requirements 3.13.0 ------ * Fix spelling of config option help * Expose sample config opts for http-proxy-to-wsgi 3.12.0 ------ * Updated from global requirements * Updated from global requirements * Do not add a default content type when replying 3.11.0 ------ * cors: remove unused import * Updated from global requirements * Add a simple \_\_main\_\_ to easily show healthcheck output * Deprecate using String as valid value for allowed\_origin * Deprecate multiple config block parsing * Deprecated set\_latent 3.10.0 ------ * Removed simple headers from documentation examples * Added PATCH method to default config * Added set\_defaults method to oslo\_middleware.cors * Set default allow methods to those defined in RFC 2616 * Removed default headers from CORS\_OPTS 3.9.0 ----- * Imported Translations from Zanata * Updated from global requirements * Remove direct dependency on babel * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * cors: prevent WebOb setting a default Content-Type * CORS Middleware now honors upstream Vary header * Disable http\_proxy\_to\_wsgi middleware by default * CORS tests now use a transient configuration * Updated config documentation for cors\_middleware * Updated from global requirements * Updated from global requirements * Revert "work around doc build error" 3.7.0 ----- * work around doc build error * Clean up removed hacking rule from [flake8] ignore lists 3.6.0 ----- * Update translation setup * Updated from global requirements * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata * Remove ordereddict from requirements.txt * Updated from global requirements 3.5.0 ----- * Remove old references to namespace packages * test: pass enforce\_type=True when using CONF.set\_override * Updated from global requirements * Change LOG.warn to LOG.warning * Fix the url in the CONTRIBUTING.rst * Python 3 deprecated the logger.warn method in favor of warning * assertIsNone(val) instead of assertEqual(None,val) 3.4.0 ----- * Updated from global requirements 3.3.0 ----- * [fix-compat] More definitions for oslo.middleware * Updated from global requirements 3.2.0 ----- * Re-Add oslo.middleware namespace for backward compat * Removes MANIFEST.in as it is not needed explicitely by PBR 3.1.0 ----- * Support Unicode request\_id on Python 3 * Drop python 2.6 support 3.0.0 ----- * Move cors allowed\_origin check into add\_origin * Backward compat for allowed\_origin * Updated from global requirements * Updated from global requirements * remove bad entry point test logic * Remove python 2.6 classifier * Updated from global requirements * Switched StrOpt to ListOpt in CORS allowed\_origins * Enable latent CORS configuration via pastedeploy * Remove python 2.6 and cleanup tox.ini * Add oslo\_config program support to paste middleware * Define entry points for filter factories for Paste Deployment 2.11.0 ------ * Remove oslo namespace package * add missing shortcut for HTTPProxyToWSGI middleware * Revert "Add 'X-Auth-Token' default header to CORS middleware" * Updated from global requirements * Imported Translations from Zanata * Add a disabled by ports -> files healthcheck plugin * Add 'X-Auth-Token' default header to CORS middleware * Updated from global requirements 2.10.0 ------ * Updated from global requirements * Updated from global requirements * Updated from global requirements 2.9.0 ----- * Fix coverage configuration and execution * No need for Oslo Incubator Sync * Allow health check results to provide there own details * ssl: deprecated in favor of http\_proxy\_to\_wsgi * Introduce HTTP Proxy to WSGI middleware * Imported Translations from Zanata * Fix string formatting * Fix typos in docstring * Include changelog/history in docs * Add shields.io version/downloads links/badges into README.rst * Change ignore-errors to ignore\_errors * Fix the home-page in setup.cfg with Oslo wiki page * Add plugin doco generated with stevedore.sphinxext * Avoid any body when request is a HEAD request * Further improve the healthcheck output * Allow the healthcheck middleware to provide more detailed responses * Updated from global requirements * Imported Translations from Zanata * cors: fix wildcard in actual request * cors: fix filter with oslo\_config\_project is set 2.8.0 ----- * Split option discovery function by middleware * Updated from global requirements * Updated from global requirements 2.7.0 ----- * Fix remarks missed on #209817 2.6.1 ----- * Added new test for CORS Simple Headers * Restore backward compat of paste factory 2.6.0 ----- * Do not dump HealthcheckResult class source * Updated from global requirements * Renamed latent\_configuration to \_latent\_configuration * Added latent properties to CORS middleware * Declare some methods static * Allow to get option from paste-deploy * Remove usage of oslo.config global * Imported Translations from Transifex * Updated from global requirements 2.5.0 ----- * Imported Translations from Transifex * Updated from global requirements * Imported Translations from Transifex * Updated from global requirements * Updated from global requirements * Updated from global requirements * Added verbose debug logging to CORS 2.4.0 ----- * Fix mocking for 1.1.0 * Updated from global requirements * Imported Translations from Transifex * Updated from global requirements * Support PasteDeploy * Add tox target to find missing requirements 2.3.0 ----- * Drop use of 'oslo' namespace package 2.2.0 ----- * CORS Middleware defers to server response * Updated from global requirements * Drop use of 'oslo' namespace package * Use correct oslo\_middleware.base methods in CORS middleware * Added request to parse\_response * Updated from global requirements * Updated from global requirements 2.1.0 ----- * Revert "Remove oslo namespace package" * Add middleware to support ssl termination proxies 2.0.0 ----- 1.3.0 ----- * Added CORS wildcard handling * Drop use of 'oslo' namespace package * Remove oslo namespace package * Updated from global requirements * Advertise support for Python3.4 / Remove support for Python 3.3 * Remove run\_cross\_tests.sh 1.2.0 ----- * Imported Translations from Transifex * Update CORS tests to use config fixture's load\_raw\_values * Updated from global requirements * Update response body when healthcheck is available 1.1.0 ----- * Uncap library requirements for liberty * Add CORS Middleware for Oslo * Update to latest hacking * Updated from global requirements 1.0.0 ----- * Updated from global requirements 0.5.0 ----- * Update the list of packages in setup.cfg * Updated from global requirements * Adds missing files 0.4.0 ----- * Fixes the healthcheck factory method and docs * add shortcut to healthcheck middleware * Updated from global requirements * Move i18n module to a private name * Update Oslo imports to remove namespace package * Add healthcheck middleware * Updated from global requirements * Fix bug tracker link in readme 0.3.0 ----- * Move files out of the namespace package * Don't use default value in LimitingReader * switch to oslo.context * Workflow documentation is now in infra-manual 0.2.0 ----- * Updated from global requirements * Updated from global requirements * Flesh out the README * Imported Translations from Transifex * Updated from global requirements * Add pbr to installation requirements * Updated from global requirements * Updated from global requirements * Remove extraneous vim editor configuration comments * Imported Translations from Transifex * Support building wheels (PEP-427) * Fix coverage testing * Expose sizelimit option to config generator * Imported Translations from Transifex * Imported Translations from Transifex * Updated from global requirements * Imported Translations from Transifex * Remove oslo-incubator fixture 0.1.0 ----- * Updated from global requirements * Setup for translation * Work toward Python 3.4 support and testing * warn against sorting requirements * Update docs for first release * expose middleware through oslo.middleware * add doc generation support * drop middleware suffix in class names * fix pep8 issues * Fixed syntax of the HACKING.rst file * Enable hacking check H305 * Cleaning up index.rst file * replace with oslo.i18n * remove stray tests * initial oslo.middleware checkin * exported from oslo-incubator by graduate.sh * pep8: fixed multiple violations * Use moxstubout and mockpatch from oslotest * Improve help strings * Use oslotest instead of common test module * sizelimit: port to Python 3 * Don't store the request ID value in middleware as class variable * Python 3: enable tests/unit/middleware/test\_request\_id.py * Update oslo log messages with translation domains * Small edits on help strings * Middleware to catch all error in WSGI pipeline * Use hacking import\_exceptions for gettextutils.\_ * Middlelware to ensure request ID * middleware.sizelimit: stop importing wsgi * Use six.iteritems to make dict work on Python2/3 * Trivial: Make vertical white space after license header consistent * Remove vim header * Remove uuidutils imports in oslo modules * Revert "Removes generate\_uuid from uuidutils" * Removes generate\_uuid from uuidutils * Replace using tests.utils part1 * Bump hacking to 0.7.0 * middleware: code cleanup * Enable H302 hacking check * Enable hacking H404 test * Move wsgi module to deprecated package * python3: python3 binary/text data compatbility * Use stubout in test\_correlation\_id * Add Middleware for correlation\_id * Add middleware to limit size of HTTP requests * Initial skeleton project ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/HACKING.rst0000664000175000017500000000023000000000000016422 0ustar00zuulzuul00000000000000oslo.middleware Style Commandments ================================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/LICENSE0000664000175000017500000002363600000000000015650 0ustar00zuulzuul00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/PKG-INFO0000664000175000017500000000376200000000000015736 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: oslo.middleware Version: 6.1.0 Summary: Oslo Middleware library Home-page: https://docs.openstack.org/oslo.middleware/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: =============== oslo.middleware =============== .. image:: https://img.shields.io/pypi/v/oslo.middleware.svg :target: https://pypi.org/project/oslo.middleware/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/oslo.middleware.svg :target: https://pypi.org/project/oslo.middleware/ :alt: Downloads Oslo middleware library includes components that can be injected into wsgi pipelines to intercept request/response flows. The base class can be enhanced with functionality like add/delete/modification of http headers and support for limiting size/connection etc. * Free software: Apache license * Documentation: https://docs.openstack.org/oslo.middleware/latest/ * Source: https://opendev.org/openstack/oslo.middleware * Bugs: https://bugs.launchpad.net/oslo.middleware * Release notes: https://docs.openstack.org/releasenotes/oslo.middleware/ Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 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 :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/README.rst0000664000175000017500000000155200000000000016323 0ustar00zuulzuul00000000000000=============== oslo.middleware =============== .. image:: https://img.shields.io/pypi/v/oslo.middleware.svg :target: https://pypi.org/project/oslo.middleware/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/oslo.middleware.svg :target: https://pypi.org/project/oslo.middleware/ :alt: Downloads Oslo middleware library includes components that can be injected into wsgi pipelines to intercept request/response flows. The base class can be enhanced with functionality like add/delete/modification of http headers and support for limiting size/connection etc. * Free software: Apache license * Documentation: https://docs.openstack.org/oslo.middleware/latest/ * Source: https://opendev.org/openstack/oslo.middleware * Bugs: https://bugs.launchpad.net/oslo.middleware * Release notes: https://docs.openstack.org/releasenotes/oslo.middleware/ ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1708612004.43355 oslo.middleware-6.1.0/doc/0000775000175000017500000000000000000000000015376 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/requirements.txt0000664000175000017500000000056700000000000020672 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # These are needed for docs generation openstackdocstheme>=2.2.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD reno>=3.1.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1708612004.43355 oslo.middleware-6.1.0/doc/source/0000775000175000017500000000000000000000000016676 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1708612004.43355 oslo.middleware-6.1.0/doc/source/admin/0000775000175000017500000000000000000000000017766 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/admin/cross-project-cors.rst0000664000175000017500000001550400000000000024266 0ustar00zuulzuul00000000000000.. _cross-project: ============================= Cross-origin resource sharing ============================= .. note:: This is a new feature in OpenStack Liberty. OpenStack supports :term:`Cross-Origin Resource Sharing (CORS)`, a W3C specification defining a contract by which the single-origin policy of a user agent (usually a browser) may be relaxed. It permits the javascript engine to access an API that does not reside on the same domain, protocol, or port. This feature is most useful to organizations which maintain one or more custom user interfaces for OpenStack, as it permits those interfaces to access the services directly, rather than requiring an intermediate proxy server. It can, however, also be misused by malicious actors; please review the security advisory below for more information. Enabling CORS with configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In most cases, CORS support is built directly into the service itself. To enable it, simply follow the configuration options exposed in the default configuration file, or add it yourself according to the pattern below. .. code-block:: ini [cors] allowed_origin = https://first_ui.example.com max_age = 3600 allow_methods = GET,POST,PUT,DELETE allow_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header expose_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header Additional origins can be explicitly added. To express this in your configuration file, first begin with a ``[cors]`` group as above, into which you place your default configuration values. Then, add as many additional configuration groups as necessary, naming them ``[cors.{something}]`` (each name must be unique). The purpose of the suffix to ``cors.`` is legibility, we recommend using a reasonable human-readable string: .. code-block:: ini [cors.ironic_webclient] # CORS Configuration for a hypothetical ironic webclient, which overrides # authentication allowed_origin = https://ironic.example.com:443 allow_credentials = True [cors.horizon] # CORS Configuration for horizon, which uses global options. allowed_origin = https://horizon.example.com:443 [cors.wildcard] # CORS Configuration for the CORS specified domain wildcard, which only # permits HTTP GET requests. allowed_origin = * allow_methods = GET For more information about CORS configuration, see `cross-origin resource sharing `_ in OpenStack Configuration Reference. Enabling CORS with PasteDeploy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CORS can also be configured using PasteDeploy. First of all, ensure that OpenStack's ``oslo_middleware`` package (version 2.4.0 or later) is available in the Python environment that is running the service. Then, add the following configuration block to your ``paste.ini`` file. .. code-block:: ini [filter:cors] paste.filter_factory = oslo_middleware.cors:filter_factory allowed_origin = https://website.example.com:443 max_age = 3600 allow_methods = GET,POST,PUT,DELETE allow_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header expose_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header .. note:: To add an additional domain in oslo_middleware v2.4.0, add another filter. In v3.0.0 and after, you may add multiple domains in the above ``allowed_origin`` field, separated by commas. Security concerns ~~~~~~~~~~~~~~~~~ CORS specifies a wildcard character ``*``, which permits access to all user agents, regardless of domain, protocol, or host. While there are valid use cases for this approach, it also permits a malicious actor to create a convincing facsimile of a user interface, and trick users into revealing authentication credentials. Please carefully evaluate your use case and the relevant documentation for any risk to your organization. .. note:: The CORS specification does not support using this wildcard as a part of a URI. Setting ``allowed_origin`` to ``*`` would work, while ``*.openstack.org`` would not. Troubleshooting ~~~~~~~~~~~~~~~ CORS is very easy to get wrong, as even one incorrect property will violate the prescribed contract. Here are some steps you can take to troubleshoot your configuration. Check the service log --------------------- The CORS middleware used by OpenStack provides verbose debug logging that should reveal most configuration problems. Here are some example log messages, and how to resolve them. Problem ------- ``CORS request from origin 'http://example.com' not permitted.`` Solution -------- A request was received from the origin ``http://example.com``, however this origin was not found in the permitted list. The cause may be a superfluous port notation (ports 80 and 443 do not need to be specified). To correct, ensure that the configuration property for this host is identical to the host indicated in the log message. Problem ------- ``Request method 'DELETE' not in permitted list: GET,PUT,POST`` Solution -------- A user agent has requested permission to perform a DELETE request, however the CORS configuration for the domain does not permit this. To correct, add this method to the ``allow_methods`` configuration property. Problem ------- ``Request header 'X-Custom-Header' not in permitted list: X-Other-Header`` Solution -------- A request was received with the header ``X-Custom-Header``, which is not permitted. Add this header to the ``allow_headers`` configuration property. Open your browser's console log ------------------------------- Most browsers provide helpful debug output when a CORS request is rejected. Usually this happens when a request was successful, but the return headers on the response do not permit access to a property which the browser is trying to access. Manually construct a CORS request --------------------------------- By using ``curl`` or a similar tool, you can trigger a CORS response with a properly constructed HTTP request. An example request and response might look like this. Request example: .. code-block:: console $ curl -I -X OPTIONS https://api.example.com/api -H "Origin: https://ui.example.com" Response example: .. code-block:: console HTTP/1.1 204 No Content Content-Length: 0 Access-Control-Allow-Origin: https://ui.example.com Access-Control-Allow-Methods: GET,POST,PUT,DELETE Access-Control-Expose-Headers: origin,authorization,accept,x-total,x-limit,x-marker,x-client,content-type Access-Control-Allow-Headers: origin,authorization,accept,x-total,x-limit,x-marker,x-client,content-type Access-Control-Max-Age: 3600 If the service does not return any access control headers, check the service log, such as ``/var/log/upstart/ironic-api.log`` for an indication on what went wrong. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/admin/index.rst0000664000175000017500000000057700000000000021640 0ustar00zuulzuul00000000000000====================== Cross-project features ====================== Many features are common to all the OpenStack services and are consistent in their configuration and deployment patterns. Unless explicitly noted, you can safely assume that the features in this chapter are supported and configured in a consistent manner. .. toctree:: :maxdepth: 2 cross-project-cors.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/conf.py0000664000175000017500000000415700000000000020204 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2020 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, 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 = [ 'sphinx.ext.autodoc', 'oslo_config.sphinxext', 'openstackdocstheme', 'stevedore.sphinxext', ] # openstackdocstheme options openstackdocs_repo_name = 'openstack/oslo.middleware' openstackdocs_bug_project = 'oslo.middleware' openstackdocs_bug_tag = '' # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. copyright = '2014, OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- 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_path = ["."] html_theme = 'openstackdocs' # html_static_path = ['static'] ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1708612004.43355 oslo.middleware-6.1.0/doc/source/configuration/0000775000175000017500000000000000000000000021545 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/configuration/index.rst0000664000175000017500000000313700000000000023412 0ustar00zuulzuul00000000000000============================= Middlewares and configuration ============================= Middlewares can be configured in multiple fashion depending of the application needs. Here is some use-cases: Configuration from the application ---------------------------------- The application code will looks like:: from oslo_middleware import sizelimit from oslo_config import cfg conf = cfg.ConfigOpts() app = sizelimit.RequestBodySizeLimiter(your_wsgi_application, conf) Configuration with paste-deploy and the oslo.config --------------------------------------------------- The paste filter (in /etc/my_app/api-paste.ini) will looks like:: [filter:sizelimit] use = egg:oslo.middleware#sizelimit # In case of the application doesn't use the global oslo.config # object. The middleware must known the app name to load # the application configuration, by setting this: # oslo_config_project = my_app # In some cases, you may need to specify the program name for the project # as well. # oslo_config_program = my_app-api The oslo.config file of the application (eg: /etc/my_app/my_app.conf) will looks like:: [oslo_middleware] max_request_body_size=1000 Configuration with pastedeploy only ----------------------------------- The paste filter (in /etc/my_app/api-paste.ini) will looks like:: [filter:sizelimit] use = egg:oslo.middleware#sizelimit max_request_body_size=1000 This will override any configuration done via oslo.config .. note:: healthcheck middleware does not yet use oslo.config, see :doc:`../reference/healthcheck_plugins` ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1708612004.43355 oslo.middleware-6.1.0/doc/source/contributor/0000775000175000017500000000000000000000000021250 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/contributor/index.rst0000664000175000017500000000012400000000000023106 0ustar00zuulzuul00000000000000============== Contributing ============== .. include:: ../../../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/glossary.rst0000664000175000017500000000074200000000000021276 0ustar00zuulzuul00000000000000======== Glossary ======== This glossary offers a list of terms and definitions to define a vocabulary for OpenStack-related concepts. C ~ .. glossary:: Cross-Origin Resource Sharing (CORS) A mechanism that allows many resources (for example, fonts, JavaScript) on a web page to be requested from another domain outside the domain from which the resource originated. In particular, JavaScript's AJAX calls can use the XMLHttpRequest mechanism. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/index.rst0000664000175000017500000000050400000000000020536 0ustar00zuulzuul00000000000000.. include:: ../../README.rst Contents ======== .. toctree:: :maxdepth: 2 install/index contributor/index configuration/index admin/index reference/index glossary Release Notes ============= Read also the `oslo.middleware Release Notes `_. ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1708612004.43355 oslo.middleware-6.1.0/doc/source/install/0000775000175000017500000000000000000000000020344 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/install/index.rst0000664000175000017500000000032500000000000022205 0ustar00zuulzuul00000000000000============ Installation ============ At the command line:: $ pip install oslo.middleware Or, if you have virtualenvwrapper installed:: $ mkvirtualenv oslo.middleware $ pip install oslo.middleware././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4375508 oslo.middleware-6.1.0/doc/source/reference/0000775000175000017500000000000000000000000020634 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/reference/api.rst0000664000175000017500000000041400000000000022136 0ustar00zuulzuul00000000000000===== API ===== .. automodule:: oslo_middleware :members: Configuration Options ===================== RequestBodySizeLimiter ~~~~~~~~~~~~~~~~~~~~~~ .. show-options:: oslo.middleware.sizelimit SSLMiddleware ~~~~~~~~~~~~~ .. show-options:: oslo.middleware.ssl ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/reference/cors.rst0000664000175000017500000000542100000000000022336 0ustar00zuulzuul00000000000000=============== CORS Middleware =============== This middleware provides a comprehensive, configurable implementation of the CORS_ (Cross Origin Resource Sharing) specification as oslo-supported python wsgi middleware. .. note:: While this middleware supports the use of the `*` wildcard origin in the specification, this feature is not recommended for security reasons. It is provided to simplify basic use of CORS, practically meaning "I don't care how this is used." In an intranet setting, this could lead to leakage of data beyond the intranet and therefore should be avoided. Quickstart ---------- First, include the middleware in your application:: from oslo_middleware import cors app = cors.CORS(your_wsgi_application) Secondly, add as many allowed origins as you would like:: app.add_origin(allowed_origin='https://website.example.com:443', allow_credentials=True, max_age=3600, allow_methods=['GET','PUT','POST','DELETE'], allow_headers=['X-Custom-Header'], expose_headers=['X-Custom-Header']) # ... add more origins here. Configuration for oslo_config ----------------------------- A factory method has been provided to simplify configuration of your CORS domain, using oslo_config:: from oslo_middleware import cors from oslo_config import cfg app = cors.CORS(your_wsgi_application, cfg.CONF) In your application's config file, then include a configuration block something like this:: [cors] allowed_origin=https://website.example.com:443,https://website2.example.com:443 max_age=3600 allow_methods=GET,POST,PUT,DELETE allow_headers=X-Custom-Header expose_headers=X-Custom-Header Configuration for pastedeploy ----------------------------- If your application is using pastedeploy, the following configuration block will add CORS support.:: [filter:cors] use = egg:oslo.middleware#cors allowed_origin=https://website.example.com:443,https://website2.example.com:443 max_age=3600 allow_methods=GET,POST,PUT,DELETE allow_headers=X-Custom-Header expose_headers=X-Custom-Header If your application is using pastedeploy, but would also like to use the existing configuration from oslo_config in order to simplify the points of configuration, this may be done as follows.:: [filter:cors] use = egg:oslo.middleware#cors oslo_config_project = oslo_project_name # Optional field, in case the program name is different from the project: oslo_config_program = oslo_project_name-api Configuration Options --------------------- .. show-options:: oslo.middleware.cors Module Documentation -------------------- .. automodule:: oslo_middleware.cors :members: .. _CORS: http://www.w3.org/TR/cors/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/reference/healthcheck_plugins.rst0000664000175000017500000000051300000000000025371 0ustar00zuulzuul00000000000000================================ Healthcheck middleware plugins ================================ .. automodule:: oslo_middleware.healthcheck :members: .. automodule:: oslo_middleware.healthcheck.disable_by_file :members: Available Plugins ------------------ .. list-plugins:: oslo.middleware.healthcheck :detailed: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/doc/source/reference/index.rst0000664000175000017500000000015400000000000022475 0ustar00zuulzuul00000000000000========================= oslo.middleware Reference ========================= .. toctree:: :glob: * ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4375508 oslo.middleware-6.1.0/oslo.middleware.egg-info/0000775000175000017500000000000000000000000021413 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/PKG-INFO0000664000175000017500000000376200000000000022520 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: oslo.middleware Version: 6.1.0 Summary: Oslo Middleware library Home-page: https://docs.openstack.org/oslo.middleware/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: =============== oslo.middleware =============== .. image:: https://img.shields.io/pypi/v/oslo.middleware.svg :target: https://pypi.org/project/oslo.middleware/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/oslo.middleware.svg :target: https://pypi.org/project/oslo.middleware/ :alt: Downloads Oslo middleware library includes components that can be injected into wsgi pipelines to intercept request/response flows. The base class can be enhanced with functionality like add/delete/modification of http headers and support for limiting size/connection etc. * Free software: Apache license * Documentation: https://docs.openstack.org/oslo.middleware/latest/ * Source: https://opendev.org/openstack/oslo.middleware * Bugs: https://bugs.launchpad.net/oslo.middleware * Release notes: https://docs.openstack.org/releasenotes/oslo.middleware/ Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 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 :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/SOURCES.txt0000664000175000017500000000655400000000000023311 0ustar00zuulzuul00000000000000.coveragerc .mailmap .pre-commit-config.yaml .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/conf.py doc/source/glossary.rst doc/source/index.rst doc/source/admin/cross-project-cors.rst doc/source/admin/index.rst doc/source/configuration/index.rst doc/source/contributor/index.rst doc/source/install/index.rst doc/source/reference/api.rst doc/source/reference/cors.rst doc/source/reference/healthcheck_plugins.rst doc/source/reference/index.rst oslo.middleware.egg-info/PKG-INFO oslo.middleware.egg-info/SOURCES.txt oslo.middleware.egg-info/dependency_links.txt oslo.middleware.egg-info/entry_points.txt oslo.middleware.egg-info/not-zip-safe oslo.middleware.egg-info/pbr.json oslo.middleware.egg-info/requires.txt oslo.middleware.egg-info/top_level.txt oslo_middleware/__init__.py oslo_middleware/_i18n.py oslo_middleware/base.py oslo_middleware/basic_auth.py oslo_middleware/catch_errors.py oslo_middleware/correlation_id.py oslo_middleware/cors.py oslo_middleware/debug.py oslo_middleware/http_proxy_to_wsgi.py oslo_middleware/opts.py oslo_middleware/request_id.py oslo_middleware/sizelimit.py oslo_middleware/stats.py oslo_middleware/version.py oslo_middleware/healthcheck/__init__.py oslo_middleware/healthcheck/__main__.py oslo_middleware/healthcheck/disable_by_file.py oslo_middleware/healthcheck/opts.py oslo_middleware/healthcheck/pluginbase.py oslo_middleware/locale/en_GB/LC_MESSAGES/oslo_middleware.po oslo_middleware/tests/__init__.py oslo_middleware/tests/test_auth_basic.py oslo_middleware/tests/test_base.py oslo_middleware/tests/test_catch_errors.py oslo_middleware/tests/test_correlation_id.py oslo_middleware/tests/test_cors.py oslo_middleware/tests/test_entry_points.py oslo_middleware/tests/test_healthcheck.py oslo_middleware/tests/test_http_proxy_to_wsgi.py oslo_middleware/tests/test_opts.py oslo_middleware/tests/test_request_id.py oslo_middleware/tests/test_sizelimit.py oslo_middleware/tests/test_stats.py releasenotes/notes/add_reno-3b4ae0789e9c45b4.yaml releasenotes/notes/basic-auth-middleware-5f812399e325425f.yaml releasenotes/notes/compat_headers-55a635b8ec01b6f1.yaml releasenotes/notes/drop-python27-support-a6361831195bf29c.yaml releasenotes/notes/global_request_id-a8ec7260fbd76444.yaml releasenotes/notes/healthcheck-allowed_source_ranges-9cbaf89f65914851.yaml releasenotes/notes/healthcheck-ignore_proxied_requests-d04d1661bd687bc6.yaml releasenotes/notes/log_max_request_body_size-1835363-6f37946210a100d7.yaml releasenotes/notes/remove-oslo-namespace-b0f050f074d12aff.yaml releasenotes/notes/remove-ssl-middleware-0ce922b05f83c447.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/dependency_links.txt0000664000175000017500000000000100000000000025461 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/entry_points.txt0000664000175000017500000000225700000000000024717 0ustar00zuulzuul00000000000000[oslo.config.opts] oslo.middleware = oslo_middleware.opts:list_opts oslo.middleware.basic_auth = oslo_middleware.opts:list_opts_basic_auth oslo.middleware.cors = oslo_middleware.opts:list_opts_cors oslo.middleware.healthcheck = oslo_middleware.opts:list_opts_healthcheck oslo.middleware.http_proxy_to_wsgi = oslo_middleware.opts:list_opts_http_proxy_to_wsgi oslo.middleware.sizelimit = oslo_middleware.opts:list_opts_sizelimit [oslo.middleware.healthcheck] disable_by_file = oslo_middleware.healthcheck.disable_by_file:DisableByFileHealthcheck disable_by_files_ports = oslo_middleware.healthcheck.disable_by_file:DisableByFilesPortsHealthcheck [paste.app_factory] healthcheck = oslo_middleware:Healthcheck.app_factory [paste.filter_factory] basic_auth = oslo_middleware:BasicAuthMiddleware.factory catch_errors = oslo_middleware:CatchErrors.factory correlation_id = oslo_middleware:CorrelationId.factory cors = oslo_middleware:CORS.factory debug = oslo_middleware:Debug.factory healthcheck = oslo_middleware:Healthcheck.factory http_proxy_to_wsgi = oslo_middleware:HTTPProxyToWSGI.factory request_id = oslo_middleware:RequestId.factory sizelimit = oslo_middleware:RequestBodySizeLimiter.factory ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/not-zip-safe0000664000175000017500000000000100000000000023641 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/pbr.json0000664000175000017500000000005600000000000023072 0ustar00zuulzuul00000000000000{"git_version": "531f39e", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/requires.txt0000664000175000017500000000027500000000000024017 0ustar00zuulzuul00000000000000Jinja2>=2.10 WebOb>=1.8.0 bcrypt>=3.1.3 debtcollector>=1.2.0 oslo.config>=5.2.0 oslo.context>=2.19.2 oslo.i18n>=3.15.3 oslo.utils>=3.33.0 pbr!=2.1.0,>=2.0.0 statsd>=3.2.1 stevedore>=1.20.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708612004.0 oslo.middleware-6.1.0/oslo.middleware.egg-info/top_level.txt0000664000175000017500000000002000000000000024135 0ustar00zuulzuul00000000000000oslo_middleware ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4415514 oslo.middleware-6.1.0/oslo_middleware/0000775000175000017500000000000000000000000020002 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/__init__.py0000664000175000017500000000241200000000000022112 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. __all__ = ['BasicAuthMiddleware', 'CatchErrors', 'CorrelationId', 'CORS', 'Debug', 'Healthcheck', 'HTTPProxyToWSGI', 'RequestId', 'RequestBodySizeLimiter'] from oslo_middleware.basic_auth import BasicAuthMiddleware from oslo_middleware.catch_errors import CatchErrors from oslo_middleware.correlation_id import CorrelationId from oslo_middleware.cors import CORS from oslo_middleware.debug import Debug from oslo_middleware.healthcheck import Healthcheck from oslo_middleware.http_proxy_to_wsgi import HTTPProxyToWSGI from oslo_middleware.request_id import RequestId from oslo_middleware.sizelimit import RequestBodySizeLimiter ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/_i18n.py0000664000175000017500000000153000000000000021271 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """oslo.i18n integration module. See https://docs.openstack.org/oslo.i18n/latest/user/index.html """ import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='oslo_middleware') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/base.py0000664000175000017500000001137000000000000021270 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Base class(es) for WSGI Middleware.""" from inspect import getfullargspec from oslo_config import cfg import webob.dec import webob.request import webob.response class NoContentTypeResponse(webob.response.Response): default_content_type = None # prevents webob assigning content type class NoContentTypeRequest(webob.request.Request): ResponseClass = NoContentTypeResponse class ConfigurableMiddleware(object): """Base WSGI middleware wrapper. These classes require an application to be initialized that will be called next. By default the middleware will simply call its wrapped app, or you can override __call__ to customize its behavior. """ @classmethod def factory(cls, global_conf, **local_conf): """Factory method for paste.deploy. :param global_conf: dict of options for all middlewares (usually the [DEFAULT] section of the paste deploy configuration file) :param local_conf: options dedicated to this middleware (usually the option defined in the middleware section of the paste deploy configuration file) """ conf = global_conf.copy() if global_conf else {} conf.update(local_conf) def middleware_filter(app): return cls(app, conf) return middleware_filter def __init__(self, application, conf=None): """Base middleware constructor :param conf: a dict of options or a cfg.ConfigOpts object """ self.application = application # NOTE(sileht): If the configuration come from oslo.config # just use it. if isinstance(conf, cfg.ConfigOpts): self.conf = {} self.oslo_conf = conf else: self.conf = conf or {} if "oslo_config_project" in self.conf: if 'oslo_config_file' in self.conf: default_config_files = [self.conf['oslo_config_file']] else: default_config_files = None if 'oslo_config_program' in self.conf: program = self.conf['oslo_config_program'] else: program = None self.oslo_conf = cfg.ConfigOpts() self.oslo_conf([], project=self.conf['oslo_config_project'], prog=program, default_config_files=default_config_files, validate_default_values=True) else: # Fallback to global object self.oslo_conf = cfg.CONF def _conf_get(self, key, group="oslo_middleware"): if key in self.conf: # Validate value type self.oslo_conf.set_override(key, self.conf[key], group=group) return getattr(getattr(self.oslo_conf, group), key) @staticmethod def process_request(req): """Called on each request. If this returns None, the next application down the stack will be executed. If it returns a response then that response will be returned and execution will stop here. """ return None @staticmethod def process_response(response, request=None): """Do whatever you'd like to the response.""" return response @webob.dec.wsgify(RequestClass=NoContentTypeRequest) def __call__(self, req): response = self.process_request(req) if response: return response response = req.get_response(self.application) args = getfullargspec(self.process_response)[0] if 'request' in args: return self.process_response(response, request=req) return self.process_response(response) class Middleware(ConfigurableMiddleware): """Legacy base WSGI middleware wrapper. Legacy interface that doesn't pass configuration options to the middleware when it's loaded via paste.deploy. """ @classmethod def factory(cls, global_conf, **local_conf): """Factory method for paste.deploy.""" return cls ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/basic_auth.py0000664000175000017500000001602100000000000022456 0ustar00zuulzuul00000000000000# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import binascii import logging import bcrypt import webob from oslo_config import cfg from oslo_middleware import base LOG = logging.getLogger(__name__) OPTS = [ cfg.StrOpt('http_basic_auth_user_file', default='/etc/htpasswd', help="HTTP basic auth password file.") ] cfg.CONF.register_opts(OPTS, group='oslo_middleware') class ConfigInvalid(Exception): def __init__(self, error_msg): super().__init__( 'Invalid configuration file. %(error_msg)s') class BasicAuthMiddleware(base.ConfigurableMiddleware): """Middleware which performs HTTP basic authentication on requests""" def __init__(self, application, conf=None): super().__init__(application, conf) self.auth_file = cfg.CONF.oslo_middleware.http_basic_auth_user_file validate_auth_file(self.auth_file) def format_exception(self, e): result = {'error': {'message': str(e), 'code': 401}} headers = [('Content-Type', 'application/json')] return webob.Response(content_type='application/json', status_code=401, json_body=result, headerlist=headers) @webob.dec.wsgify def __call__(self, req): try: token = parse_header(req.environ) username, password = parse_token(token) req.environ.update(authenticate( self.auth_file, username, password)) return self.application except Exception as e: response = self.format_exception(e) return self.process_response(response) def authenticate(auth_file, username, password): """Finds username and password match in Apache style user auth file The user auth file format is expected to comply with Apache documentation[1] however the bcrypt password digest is the *only* digest format supported. [1] https://httpd.apache.org/docs/current/misc/password_encryptions.html :param: auth_file: Path to user auth file :param: username: Username to authenticate :param: password: Password encoded as bytes :returns: A dictionary of WSGI environment values to append to the request :raises: HTTPUnauthorized, if no file entries match username/password """ line_prefix = username + ':' try: with open(auth_file, 'r') as f: for line in f: entry = line.strip() if entry and entry.startswith(line_prefix): return auth_entry(entry, password) except OSError as exc: LOG.error('Problem reading auth file: %s', exc) raise webob.exc.HTTPBadRequest( detail='Problem reading auth file') # reached end of file with no matches LOG.info('User %s not found', username) raise webob.exc.HTTPUnauthorized() def auth_entry(entry, password): """Compare a password with a single user auth file entry :param: entry: Line from auth user file to use for authentication :param: password: Password encoded as bytes :returns: A dictionary of WSGI environment values to append to the request :raises: HTTPUnauthorized, if the entry doesn't match supplied password or if the entry is crypted with a method other than bcrypt """ username, crypted = parse_entry(entry) if not bcrypt.checkpw(password, crypted): LOG.info('Password for %s does not match', username) raise webob.exc.HTTPUnauthorized() return { 'HTTP_X_USER': username, 'HTTP_X_USER_NAME': username } def validate_auth_file(auth_file): """Read the auth user file and validate its correctness :param: auth_file: Path to user auth file :raises: ConfigInvalid on validation error """ try: with open(auth_file, 'r') as f: for line in f: entry = line.strip() if entry and ':' in entry: parse_entry(entry) except OSError: raise ConfigInvalid(error_msg='Problem reading auth user file') def parse_entry(entry): """Extrace the username and crypted password from a user auth file entry :param: entry: Line from auth user file to use for authentication :returns: a tuple of username and crypted password :raises: ConfigInvalid if the password is not in the supported bcrypt format """ username, crypted_str = entry.split(':', maxsplit=1) crypted = crypted_str.encode('utf-8') if crypted[:4] not in (b'$2y$', b'$2a$', b'$2b$'): error_msg = ('Only bcrypt digested passwords are supported for ' '%(username)s') % {'username': username} raise webob.exc.HTTPBadRequest(detail=error_msg) return username, crypted def parse_token(token): """Parse the token portion of the Authentication header value :param: token: Token value from basic authorization header :returns: tuple of username, password :raises: BadRequest, if username and password could not be parsed for any reason """ try: if isinstance(token, str): token = token.encode('utf-8') auth_pair = base64.b64decode(token, validate=True) (username, password) = auth_pair.split(b':', maxsplit=1) return (username.decode('utf-8'), password) except (TypeError, binascii.Error, ValueError) as exc: LOG.info('Could not decode authorization token: %s', exc) raise webob.exc.HTTPBadRequest(detail=( 'Could not decode authorization token')) def parse_header(env): """Parse WSGI environment for Authorization header of type Basic :param: env: WSGI environment to get header from :returns: Token portion of the header value :raises: HTTPUnauthorized, if header is missing or if the type is not Basic """ try: auth_header = env.pop('HTTP_AUTHORIZATION') except KeyError: LOG.info('No authorization token received') raise webob.exc.HTTPUnauthorized() try: auth_type, token = auth_header.strip().split(maxsplit=1) except (ValueError, AttributeError) as exc: LOG.info('Could not parse Authorization header: %s', exc) raise webob.exc.HTTPBadRequest(detail=( 'Could not parse Authorization header')) if auth_type.lower() != 'basic': error_msg = ('Unsupported authorization type "%s"') % auth_type LOG.info(error_msg) raise webob.exc.HTTPBadRequest(detail=error_msg) return token ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/catch_errors.py0000664000175000017500000000272100000000000023034 0ustar00zuulzuul00000000000000# Copyright (c) 2013 NEC Corporation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import re import webob.dec import webob.exc from oslo_middleware import base LOG = logging.getLogger(__name__) _TOKEN_RE = re.compile(r'^(X-\w+-Token):.*$', flags=re.MULTILINE) class CatchErrors(base.ConfigurableMiddleware): """Middleware that provides high-level error handling. It catches all exceptions from subsequent applications in WSGI pipeline to hide internal errors from API response. """ @webob.dec.wsgify def __call__(self, req): try: response = req.get_response(self.application) except Exception: req_str = _TOKEN_RE.sub(r'\1: *****', req.as_text()) LOG.exception('An error occurred during ' 'processing the request: %s', req_str) response = webob.exc.HTTPInternalServerError() return response ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/correlation_id.py0000664000175000017500000000201000000000000023342 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Rackspace Hosting # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_utils import uuidutils from oslo_middleware import base class CorrelationId(base.ConfigurableMiddleware): "Middleware that attaches a correlation id to WSGI request" def process_request(self, req): correlation_id = (req.headers.get("X_CORRELATION_ID") or uuidutils.generate_uuid()) req.headers['X_CORRELATION_ID'] = correlation_id ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/cors.py0000664000175000017500000004022100000000000021321 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. See the License for the specific language governing permissions and # limitations under the License. import copy import logging import debtcollector from oslo_config import cfg from oslo_middleware import base import webob.exc LOG = logging.getLogger(__name__) CORS_OPTS = [ cfg.ListOpt('allowed_origin', help='Indicate whether this resource may be shared with the ' 'domain received in the requests "origin" header. ' 'Format: "://[:]", no trailing ' 'slash. Example: https://horizon.example.com'), cfg.BoolOpt('allow_credentials', default=True, help='Indicate that the actual request can include user ' 'credentials'), cfg.ListOpt('expose_headers', default=[], help='Indicate which headers are safe to expose to the API. ' 'Defaults to HTTP Simple Headers.'), cfg.IntOpt('max_age', default=3600, help='Maximum cache age of CORS preflight requests.'), cfg.ListOpt('allow_methods', default=['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'PATCH'], # RFC 2616, RFC 5789 help='Indicate which methods can be used during the actual ' 'request.'), cfg.ListOpt('allow_headers', default=[], help='Indicate which header field names may be used during ' 'the actual request.') ] def set_defaults(**kwargs): """Override the default values for configuration options. This method permits a project to override the default CORS option values. For example, it may wish to offer a set of sane default headers which allow it to function with only minimal additional configuration. :param allow_credentials: Whether to permit credentials. :type allow_credentials: bool :param expose_headers: A list of headers to expose. :type expose_headers: List of Strings :param max_age: Maximum cache duration in seconds. :type max_age: Int :param allow_methods: List of HTTP methods to permit. :type allow_methods: List of Strings :param allow_headers: List of HTTP headers to permit from the client. :type allow_headers: List of Strings """ # Since 'None' is a valid config override, we have to use kwargs. Else # there's no good way for a user to override only one option, because all # the others would be overridden to 'None'. valid_params = set(k.name for k in CORS_OPTS if k.name != 'allowed_origin') passed_params = set(k for k in kwargs) wrong_params = passed_params - valid_params if wrong_params: raise AttributeError('Parameter(s) [%s] invalid, please only use [%s]' % (wrong_params, valid_params)) # Set global defaults. cfg.set_defaults(CORS_OPTS, **kwargs) class InvalidOriginError(Exception): """Exception raised when Origin is invalid.""" def __init__(self, origin): self.origin = origin super(InvalidOriginError, self).__init__( 'CORS request from origin \'%s\' not permitted.' % origin) class CORS(base.ConfigurableMiddleware): """CORS Middleware. This middleware allows a WSGI app to serve CORS headers for multiple configured domains. For more information, see http://www.w3.org/TR/cors/ """ simple_headers = [ 'Accept', 'Accept-Language', 'Content-Type', 'Cache-Control', 'Content-Language', 'Expires', 'Last-Modified', 'Pragma' ] def __init__(self, application, *args, **kwargs): super(CORS, self).__init__(application, *args, **kwargs) # Begin constructing our configuration hash. self.allowed_origins = {} self._init_conf() def sanitize(csv_list): try: return [str.strip(x) for x in csv_list.split(',')] except Exception: return None @classmethod def factory(cls, global_conf, **local_conf): """factory method for paste.deploy allowed_origin: Protocol, host, and port for the allowed origin. allow_credentials: Whether to permit credentials. expose_headers: A list of headers to expose. max_age: Maximum cache duration. allow_methods: List of HTTP methods to permit. allow_headers: List of HTTP headers to permit from the client. """ if ('allowed_origin' not in local_conf and 'oslo_config_project' not in local_conf): raise TypeError("allowed_origin or oslo_config_project " "is required") return super(CORS, cls).factory(global_conf, **local_conf) def _init_conf(self): '''Initialize this middleware from an oslo.config instance.''' # First, check the configuration and register global options. self.oslo_conf.register_opts(CORS_OPTS, 'cors') allowed_origin = self._conf_get('allowed_origin', 'cors') allow_credentials = self._conf_get('allow_credentials', 'cors') expose_headers = self._conf_get('expose_headers', 'cors') max_age = self._conf_get('max_age', 'cors') allow_methods = self._conf_get('allow_methods', 'cors') allow_headers = self._conf_get('allow_headers', 'cors') # Clone our original CORS_OPTS, and set the defaults to whatever is # set in the global conf instance. This is done explicitly (instead # of **kwargs), since we don't accidentally want to catch # allowed_origin. subgroup_opts = copy.deepcopy(CORS_OPTS) cfg.set_defaults(subgroup_opts, allow_credentials=allow_credentials, expose_headers=expose_headers, max_age=max_age, allow_methods=allow_methods, allow_headers=allow_headers) # If the default configuration contains an allowed_origin, don't # forget to register that. self.add_origin(allowed_origin=allowed_origin, allow_credentials=allow_credentials, expose_headers=expose_headers, max_age=max_age, allow_methods=allow_methods, allow_headers=allow_headers) # Iterate through all the loaded config sections, looking for ones # prefixed with 'cors.' for section in self.oslo_conf.list_all_sections(): if section.startswith('cors.'): debtcollector.deprecate('Multiple configuration blocks are ' 'deprecated and will be removed in ' 'future versions. Please consolidate ' 'your configuration in the [cors] ' 'configuration block.') # Register with the preconstructed defaults self.oslo_conf.register_opts(subgroup_opts, section) self.add_origin(**self.oslo_conf[section]) def add_origin(self, allowed_origin, allow_credentials=True, expose_headers=None, max_age=None, allow_methods=None, allow_headers=None): '''Add another origin to this filter. :param allowed_origin: Protocol, host, and port for the allowed origin. :param allow_credentials: Whether to permit credentials. :param expose_headers: A list of headers to expose. :param max_age: Maximum cache duration. :param allow_methods: List of HTTP methods to permit. :param allow_headers: List of HTTP headers to permit from the client. :return: ''' # NOTE(dims): Support older code that still passes in # a string for allowed_origin instead of a list if isinstance(allowed_origin, str): # TODO(krotscheck): https://review.opendev.org/#/c/312687/ LOG.warning('DEPRECATED: The `allowed_origin` keyword argument in ' '`add_origin()` should be a list, found String.') allowed_origin = [allowed_origin] if allowed_origin: for origin in allowed_origin: if origin in self.allowed_origins: LOG.warning('Allowed origin [%s] already exists, skipping' % (allowed_origin,)) continue self.allowed_origins[origin] = { 'allow_credentials': allow_credentials, 'expose_headers': expose_headers, 'max_age': max_age, 'allow_methods': allow_methods, 'allow_headers': allow_headers } def process_response(self, response, request=None): '''Check for CORS headers, and decorate if necessary. Perform two checks. First, if an OPTIONS request was issued, let the application handle it, and (if necessary) decorate the response with preflight headers. In this case, if a 404 is thrown by the underlying application (i.e. if the underlying application does not handle OPTIONS requests, the response code is overridden. In the case of all other requests, regular request headers are applied. ''' # Sanity precheck: If we detect CORS headers provided by something in # in the middleware chain, assume that it knows better. if 'Access-Control-Allow-Origin' in response.headers: return response # Doublecheck for an OPTIONS request. if request.method == 'OPTIONS': return self._apply_cors_preflight_headers(request=request, response=response) # Apply regular CORS headers. self._apply_cors_request_headers(request=request, response=response) # Finally, return the response. return response @staticmethod def _split_header_values(request, header_name): """Convert a comma-separated header value into a list of values.""" values = [] if header_name in request.headers: for value in request.headers[header_name].rsplit(','): value = value.strip() if value: values.append(value) return values def _apply_cors_preflight_headers(self, request, response): """Handle CORS Preflight (Section 6.2) Given a request and a response, apply the CORS preflight headers appropriate for the request. """ # If the response contains a 2XX code, we have to assume that the # underlying middleware's response content needs to be persisted. # Otherwise, create a new response. if 200 > response.status_code or response.status_code >= 300: response = base.NoContentTypeResponse(status=webob.exc.HTTPOk.code) # Does the request have an origin header? (Section 6.2.1) if 'Origin' not in request.headers: return response # Is this origin registered? (Section 6.2.2) try: origin, cors_config = self._get_cors_config_by_origin( request.headers['Origin']) except InvalidOriginError: return response # If there's no request method, exit. (Section 6.2.3) if 'Access-Control-Request-Method' not in request.headers: LOG.debug('CORS request does not contain ' 'Access-Control-Request-Method header.') return response request_method = request.headers['Access-Control-Request-Method'] # Extract Request headers. If parsing fails, exit. (Section 6.2.4) try: request_headers = \ self._split_header_values(request, 'Access-Control-Request-Headers') except Exception: LOG.debug('Cannot parse request headers.') return response # Compare request method to permitted methods (Section 6.2.5) permitted_methods = cors_config['allow_methods'] if request_method not in permitted_methods: LOG.debug('Request method \'%s\' not in permitted list: %s' % (request_method, permitted_methods)) return response # Compare request headers to permitted headers, case-insensitively. # (Section 6.2.6) permitted_headers = [header.upper() for header in (cors_config['allow_headers'] + self.simple_headers)] for requested_header in request_headers: upper_header = requested_header.upper() if upper_header not in permitted_headers: LOG.debug('Request header \'%s\' not in permitted list: %s' % (requested_header, permitted_headers)) return response # Set the default origin permission headers. (Sections 6.2.7, 6.4) response.headers['Vary'] = 'Origin' response.headers['Access-Control-Allow-Origin'] = origin # Does this CORS configuration permit credentials? (Section 6.2.7) if cors_config['allow_credentials']: response.headers['Access-Control-Allow-Credentials'] = 'true' # Attach Access-Control-Max-Age if appropriate. (Section 6.2.8) if 'max_age' in cors_config and cors_config['max_age']: response.headers['Access-Control-Max-Age'] = \ str(cors_config['max_age']) # Attach Access-Control-Allow-Methods. (Section 6.2.9) response.headers['Access-Control-Allow-Methods'] = request_method # Attach Access-Control-Allow-Headers. (Section 6.2.10) if request_headers: response.headers['Access-Control-Allow-Headers'] = \ ','.join(request_headers) return response def _get_cors_config_by_origin(self, origin): if origin not in self.allowed_origins: if '*' in self.allowed_origins: origin = '*' else: LOG.debug('CORS request from origin \'%s\' not permitted.' % origin) raise InvalidOriginError(origin) return origin, self.allowed_origins[origin] def _apply_cors_request_headers(self, request, response): """Handle Basic CORS Request (Section 6.1) Given a request and a response, apply the CORS headers appropriate for the request to the response. """ # Does the request have an origin header? (Section 6.1.1) if 'Origin' not in request.headers: return # Is this origin registered? (Section 6.1.2) try: origin, cors_config = self._get_cors_config_by_origin( request.headers['Origin']) except InvalidOriginError: return # Set the default origin permission headers. (Sections 6.1.3 & 6.4) if 'Vary' in response.headers: response.headers['Vary'] += ',Origin' else: response.headers['Vary'] = 'Origin' response.headers['Access-Control-Allow-Origin'] = origin # Does this CORS configuration permit credentials? (Section 6.1.3) if cors_config['allow_credentials']: response.headers['Access-Control-Allow-Credentials'] = 'true' # Attach the exposed headers and exit. (Section 6.1.4) if cors_config['expose_headers']: response.headers['Access-Control-Expose-Headers'] = \ ','.join(cors_config['expose_headers']) # NOTE(sileht): Shortcut for backwards compatibility filter_factory = CORS.factory ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/debug.py0000664000175000017500000000326600000000000021451 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Debug middleware""" import sys import webob.dec from oslo_middleware import base class Debug(base.ConfigurableMiddleware): """Helper class that returns debug information. Can be inserted into any WSGI application chain to get information about the request and response. """ @webob.dec.wsgify def __call__(self, req): print(("*" * 40) + " REQUEST ENVIRON") for key, value in req.environ.items(): print(key, "=", value) print() resp = req.get_response(self.application) print(("*" * 40) + " RESPONSE HEADERS") for (key, value) in resp.headers.items(): print(key, "=", value) print() resp.app_iter = self.print_generator(resp.app_iter) return resp @staticmethod def print_generator(app_iter): """Prints the contents of a wrapper string iterator when iterated.""" print(("*" * 40) + " BODY") for part in app_iter: sys.stdout.write(part) sys.stdout.flush() yield part print() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4415514 oslo.middleware-6.1.0/oslo_middleware/healthcheck/0000775000175000017500000000000000000000000022245 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/healthcheck/__init__.py0000664000175000017500000004326000000000000024363 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import gc import io import ipaddress import json import platform import socket import sys import traceback from debtcollector import removals import jinja2 from oslo_utils import reflection from oslo_utils import strutils from oslo_utils import timeutils import stevedore import webob.dec import webob.exc import webob.response try: import greenlet except ImportError: greenlet = None from oslo_middleware import base from oslo_middleware.healthcheck import opts def _find_objects(t): return [o for o in gc.get_objects() if isinstance(o, t)] def _expand_template(contents, params): tpl = jinja2.Template(source=contents, undefined=jinja2.StrictUndefined) return tpl.render(**params) class Healthcheck(base.ConfigurableMiddleware): """Healthcheck application used for monitoring. It will respond 200 with "OK" as the body. Or a 503 with the reason as the body if one of the backends reports an application issue. This is useful for the following reasons: * Load balancers can 'ping' this url to determine service availability. * Provides an endpoint that is similar to 'mod_status' in apache which can provide details (or no details, depending on if configured) about the activity of the server. * *(and more)* .. note:: This middleware indicates that the API is accessible but it does indicate that it is necessarily functional or that any other API request will actually work. Example requests/responses (**not** detailed mode):: $ curl -i -X HEAD "http://0.0.0.0:8775/healthcheck" HTTP/1.1 204 No Content Content-Type: text/plain; charset=UTF-8 Content-Length: 0 Date: Fri, 11 Sep 2015 18:55:08 GMT $ curl -i -X GET "http://0.0.0.0:8775/healthcheck" HTTP/1.1 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 2 Date: Fri, 11 Sep 2015 18:55:43 GMT OK $ curl -X GET -i -H "Accept: application/json" "http://0.0.0.0:8775/healthcheck" HTTP/1.0 200 OK Date: Wed, 24 Aug 2016 06:09:58 GMT Content-Type: application/json Content-Length: 63 { "detailed": false, "reasons": [ "OK" ] } $ curl -X GET -i -H "Accept: text/html" "http://0.0.0.0:8775/healthcheck" HTTP/1.0 200 OK Date: Wed, 24 Aug 2016 06:10:42 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 239 Healthcheck Status

Result of 1 checks:

Reason
OK

Example requests/responses (**detailed** mode):: $ curl -X GET -i -H "Accept: application/json" "http://0.0.0.0:8775/healthcheck" HTTP/1.0 200 OK Date: Wed, 24 Aug 2016 06:11:59 GMT Content-Type: application/json Content-Length: 3480 { "detailed": true, "gc": { "counts": [ 293, 10, 5 ], "threshold": [ 700, 10, 10 ] }, "greenthreads": [ ... ], "now": "2016-08-24 06:11:59.419267", "platform": "Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty", "python_version": "2.7.6 (default, Jun 22 2015, 17:58:13) \\n[GCC 4.8.2]", "reasons": [ { "class": "HealthcheckResult", "details": "Path '/tmp/dead' was not found", "reason": "OK" } ], "threads": [ ... ] } $ curl -X GET -i -H "Accept: text/html" "http://0.0.0.0:8775/healthcheck" HTTP/1.0 200 OK Date: Wed, 24 Aug 2016 06:36:07 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 6838 Healthcheck Status

Server status

Server hostname:
...
Current time:
2016-08-24 06:36:07.302559
Python version:
2.7.6 (default, Jun 22 2015, 17:58:13)
       [GCC 4.8.2]
Platform:
Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty

Garbage collector:

Counts:
(77, 1, 6)
Thresholds:
(700, 10, 10)

Result of 1 checks:

Kind Reason Details
HealthcheckResult OK Path '/tmp/dead' was not found

1 greenthread(s) active:

  File "oslo_middleware/healthcheck/__main__.py", line 94, in <module>
           main()
         File "oslo_middleware/healthcheck/__main__.py", line 90, in main
           server.serve_forever()
         ...
       

1 thread(s) active:

  File "oslo_middleware/healthcheck/__main__.py", line 94, in <module>
           main()
         File "oslo_middleware/healthcheck/__main__.py", line 90, in main
           server.serve_forever()
         ....
       
Example of paste configuration: .. code-block:: ini [app:healthcheck] use = egg:oslo.middleware:healthcheck backends = disable_by_file disable_by_file_path = /var/run/nova/healthcheck_disable [pipeline:public_api] pipeline = healthcheck sizelimit [...] public_service Multiple filter sections can be defined if it desired to have pipelines with different healthcheck configuration, example: .. code-block:: ini [composite:public_api] use = egg:Paste#urlmap / = public_api_pipeline /healthcheck = healthcheck_public [composite:admin_api] use = egg:Paste#urlmap / = admin_api_pipeline /healthcheck = healthcheck_admin [pipeline:public_api_pipeline] pipeline = sizelimit [...] public_service [pipeline:admin_api_pipeline] pipeline = sizelimit [...] admin_service [app:healthcheck_public] use = egg:oslo.middleware:healthcheck backends = disable_by_file disable_by_file_path = /var/run/nova/healthcheck_public_disable [filter:healthcheck_admin] use = egg:oslo.middleware:healthcheck backends = disable_by_file disable_by_file_path = /var/run/nova/healthcheck_admin_disable """ NAMESPACE = "oslo.middleware.healthcheck" HEALTHY_TO_STATUS_CODES = { True: webob.exc.HTTPOk.code, False: webob.exc.HTTPServiceUnavailable.code, } HEAD_HEALTHY_TO_STATUS_CODES = { True: webob.exc.HTTPNoContent.code, False: webob.exc.HTTPServiceUnavailable.code, } PLAIN_RESPONSE_TEMPLATE = """ {% for reason in reasons %} {% if reason %}{{reason}}{% endif -%} {% endfor %} """ HTML_RESPONSE_TEMPLATE = """ Healthcheck Status {% if detailed -%}

Server status

{% if hostname -%} Server hostname:
{{hostname|e}}
{%- endif %} Current time:
{{now|e}}
Python version:
{{python_version|e}}
Platform:
{{platform|e}}

Garbage collector:

Counts:
{{gc.counts|e}}
Thresholds:
{{gc.threshold|e}}

{%- endif %}

Result of {{results|length}} checks:

{% if detailed -%} {% else %} {%- endif %} {% for result in results -%} {% if result.reason -%} {% if detailed -%} {%- endif %} {% if detailed -%} {%- endif %} {%- endif %} {%- endfor %}
Kind Reason Details Reason
{{result.class|e}}{{result.reason|e}}{{result.details|e}}

{% if detailed -%} {% if greenthreads -%}

{{greenthreads|length}} greenthread(s) active:

{% for stack in greenthreads -%} {%- endfor %}
{{stack|e}}

{%- endif %} {% if threads -%}

{{threads|length}} thread(s) active:

{% for stack in threads -%} {%- endfor %}
{{stack|e}}
{%- endif %} {%- endif %} """ def __init__(self, *args, **kwargs): super(Healthcheck, self).__init__(*args, **kwargs) self.oslo_conf.register_opts(opts.HEALTHCHECK_OPTS, group='healthcheck') self._path = self._conf_get('path') self._show_details = self._conf_get('detailed') self._source_ranges = [ ipaddress.ip_network(r) for r in self._conf_get('allowed_source_ranges')] self._ignore_proxied_requests = self._conf_get( 'ignore_proxied_requests') self._backends = stevedore.NamedExtensionManager( self.NAMESPACE, self._conf_get('backends'), name_order=True, invoke_on_load=True, invoke_args=(self.oslo_conf, self.conf)) self._accept_to_functor = collections.OrderedDict([ # Order here matters... ('text/plain', self._make_text_response), ('text/html', self._make_html_response), ('application/json', self._make_json_response), ]) self._accept_order = tuple(self._accept_to_functor) # When no accept type matches instead of returning 406 we will # always return text/plain (because sending an error from this # middleware actually can cause issues). self._default_accept = 'text/plain' self._ignore_path = False def _conf_get(self, key, group='healthcheck'): return super(Healthcheck, self)._conf_get(key, group=group) @removals.remove( message="The healthcheck middleware must now be configured as " "an application, not as a filter") @classmethod def factory(cls, global_conf, **local_conf): return super(Healthcheck, cls).factory(global_conf, **local_conf) @classmethod def app_factory(cls, global_conf, **local_conf): """Factory method for paste.deploy. :param global_conf: dict of options for all middlewares (usually the [DEFAULT] section of the paste deploy configuration file) :param local_conf: options dedicated to this middleware (usually the option defined in the middleware section of the paste deploy configuration file) """ conf = global_conf.copy() if global_conf else {} conf.update(local_conf) o = cls(application=None, conf=conf) o._ignore_path = True return o @staticmethod def _get_threadstacks(): threadstacks = [] try: active_frames = sys._current_frames() except AttributeError: pass else: buf = io.StringIO() for stack in active_frames.values(): traceback.print_stack(stack, file=buf) threadstacks.append(buf.getvalue()) buf.seek(0) buf.truncate() return threadstacks @staticmethod def _get_greenstacks(): greenstacks = [] if greenlet is not None: buf = io.StringIO() for gt in _find_objects(greenlet.greenlet): traceback.print_stack(gt.gr_frame, file=buf) greenstacks.append(buf.getvalue()) buf.seek(0) buf.truncate() return greenstacks @staticmethod def _pretty_json_dumps(contents): return json.dumps(contents, indent=4, sort_keys=True) @staticmethod def _are_results_healthy(results): for result in results: if not result.available: return False return True def _make_text_response(self, results, healthy): params = { 'reasons': [result.reason for result in results], 'detailed': self._show_details, } body = _expand_template(self.PLAIN_RESPONSE_TEMPLATE, params) return (body.strip(), 'text/plain') def _make_json_response(self, results, healthy): if self._show_details: body = { 'detailed': True, 'python_version': sys.version, 'now': str(timeutils.utcnow()), 'platform': platform.platform(), 'gc': { 'counts': gc.get_count(), 'threshold': gc.get_threshold(), }, } reasons = [] for result in results: reasons.append({ 'reason': result.reason, 'details': result.details or '', 'class': reflection.get_class_name(result, fully_qualified=False), }) body['reasons'] = reasons body['greenthreads'] = self._get_greenstacks() body['threads'] = self._get_threadstacks() else: body = { 'reasons': [result.reason for result in results], 'detailed': False, } return (self._pretty_json_dumps(body), 'application/json') def _make_head_response(self, results, healthy): return ( "", "text/plain") def _make_html_response(self, results, healthy): try: hostname = socket.gethostname() except socket.error: hostname = None translated_results = [] for result in results: translated_results.append({ 'details': result.details or '', 'reason': result.reason, 'class': reflection.get_class_name(result, fully_qualified=False), }) params = { 'healthy': healthy, 'hostname': hostname, 'results': translated_results, 'detailed': self._show_details, 'now': str(timeutils.utcnow()), 'python_version': sys.version, 'platform': platform.platform(), 'gc': { 'counts': gc.get_count(), 'threshold': gc.get_threshold(), }, 'threads': self._get_threadstacks(), 'greenthreads': self._get_threadstacks(), } body = _expand_template(self.HTML_RESPONSE_TEMPLATE, params) return (body.strip(), 'text/html') @webob.dec.wsgify def process_request(self, req): if not self._ignore_path and req.path != self._path: return None if self._source_ranges: remote_addr = ipaddress.ip_address(req.remote_addr) for r in self._source_ranges: if r.version == remote_addr.version and remote_addr in r: break else: # Because source ip is not included in allowed ranges, ignore # the request in this middleware. return None if self._ignore_proxied_requests: for hdr in [ 'FORWARDED', 'FORWARDED_PROTO', 'FORWARDED_HOST', 'FORWARDED_FOR', 'FORWARDED_PREFIX']: if req.environ.get("HTTP_X_%s" % hdr): return None results = [ext.obj.healthcheck(req.server_port) for ext in self._backends] healthy = self._are_results_healthy(results) if req.method == "HEAD": functor = self._make_head_response status = self.HEAD_HEALTHY_TO_STATUS_CODES[healthy] else: status = self.HEALTHY_TO_STATUS_CODES[healthy] try: offers = req.accept.acceptable_offers(self._accept_order) accept_type = offers[0][0] except IndexError: accept_type = self._default_accept functor = self._accept_to_functor[accept_type] body, content_type = functor(results, healthy) return webob.response.Response(status=status, body=body, charset='UTF-8', content_type=content_type) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/healthcheck/__main__.py0000664000175000017500000000432100000000000024337 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse from http import server import socketserver import webob from oslo_middleware import healthcheck class HttpHandler(server.SimpleHTTPRequestHandler): def do_GET(self): @webob.dec.wsgify def dummy_application(req): return 'test' app = healthcheck.Healthcheck(dummy_application, {'detailed': True}) req = webob.Request.blank("/healthcheck", accept='text/html', method='GET') res = req.get_response(app) self.send_response(res.status_code) for header_name, header_value in res.headerlist: self.send_header(header_name, header_value) self.end_headers() self.wfile.write(res.body) self.wfile.close() def positive_int(blob): value = int(blob) if value < 0: msg = "%r is not a positive integer" % blob raise argparse.ArgumentTypeError(msg) return value def create_server(port=0): handler = HttpHandler server = socketserver.TCPServer(("", port), handler) return server def main(args=None): """Runs a basic http server to show healthcheck functionality.""" parser = argparse.ArgumentParser() parser.add_argument("-p", "--port", help="Unused port to run the tiny" " http server on (or zero to select a" " random unused port)", type=positive_int, required=True) args = parser.parse_args(args=args) server = create_server(args.port) print("Serving at port: %s" % server.server_address[1]) server.serve_forever() if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/healthcheck/disable_by_file.py0000664000175000017500000001127000000000000025714 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import os from oslo_middleware.healthcheck import opts from oslo_middleware.healthcheck import pluginbase LOG = logging.getLogger(__name__) class DisableByFilesPortsHealthcheck(pluginbase.HealthcheckBaseExtension): """DisableByFilesPorts healthcheck middleware plugin This plugin checks presence of a file that is provided for a application running on a certain port to report if the service is unavailable or not. Example of middleware configuration: .. code-block:: ini [filter:healthcheck] paste.filter_factory = oslo_middleware:Healthcheck.factory path = /healthcheck backends = disable_by_files_ports disable_by_file_paths = 5000:/var/run/keystone/healthcheck_disable, \ 35357:/var/run/keystone/admin_healthcheck_disable # set to True to enable detailed output, False is the default detailed = False """ def __init__(self, *args, **kwargs): super(DisableByFilesPortsHealthcheck, self).__init__(*args, **kwargs) self.oslo_conf.register_opts(opts.DISABLE_BY_FILES_OPTS, group='healthcheck') self.status_files = {} paths = self._conf_get('disable_by_file_paths') self.status_files.update(self._iter_paths_ports(paths)) @staticmethod def _iter_paths_ports(paths): for port_path in paths: port_path = port_path.strip() if port_path: # On windows, drive letters are followed by colons, # which makes split() return 3 elements in this case port, path = port_path.split(":", 1) port = int(port) yield (port, path) def healthcheck(self, server_port): path = self.status_files.get(server_port) if not path: LOG.warning('DisableByFilesPorts healthcheck middleware' ' enabled without disable_by_file_paths set' ' for port %s', server_port) return pluginbase.HealthcheckResult(available=True, reason="OK") else: if not os.path.exists(path): return pluginbase.HealthcheckResult(available=True, reason="OK") else: return pluginbase.HealthcheckResult(available=False, reason="DISABLED BY FILE") class DisableByFileHealthcheck(pluginbase.HealthcheckBaseExtension): """DisableByFile healthcheck middleware plugin This plugin checks presence of a file to report if the service is unavailable or not. Example of middleware configuration: .. code-block:: ini [filter:healthcheck] paste.filter_factory = oslo_middleware:Healthcheck.factory path = /healthcheck backends = disable_by_file disable_by_file_path = /var/run/nova/healthcheck_disable # set to True to enable detailed output, False is the default detailed = False """ def __init__(self, *args, **kwargs): super(DisableByFileHealthcheck, self).__init__(*args, **kwargs) self.oslo_conf.register_opts(opts.DISABLE_BY_FILE_OPTS, group='healthcheck') def healthcheck(self, server_port): path = self._conf_get('disable_by_file_path') if not path: LOG.warning('DisableByFile healthcheck middleware enabled ' 'without disable_by_file_path set') return pluginbase.HealthcheckResult( available=True, reason="OK", details="No 'disable_by_file_path' configuration value" " specified") elif not os.path.exists(path): return pluginbase.HealthcheckResult( available=True, reason="OK", details="Path '%s' was not found" % path) else: return pluginbase.HealthcheckResult( available=False, reason="DISABLED BY FILE", details="Path '%s' was found" % path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/healthcheck/opts.py0000664000175000017500000000467600000000000023621 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_config import cfg HEALTHCHECK_OPTS = [ cfg.StrOpt('path', default='/healthcheck', deprecated_for_removal=True, help='The path to respond to healtcheck requests on.'), cfg.BoolOpt('detailed', default=False, help='Show more detailed information as part of the response. ' 'Security note: Enabling this option may expose ' 'sensitive details about the service being monitored. ' 'Be sure to verify that it will not violate your ' 'security policies.'), cfg.ListOpt('backends', default=[], help='Additional backends that can perform health checks and ' 'report that information back as part of a request.'), cfg.ListOpt('allowed_source_ranges', default=[], help='A list of network addresses to limit source ip allowed ' 'to access healthcheck information. Any request from ip ' 'outside of these network addresses are ignored.'), cfg.BoolOpt('ignore_proxied_requests', default=False, help='Ignore requests with proxy headers.') ] DISABLE_BY_FILE_OPTS = [ cfg.StrOpt('disable_by_file_path', default=None, help='Check the presence of a file to determine if an ' 'application is running on a port. Used by ' 'DisableByFileHealthcheck plugin.'), ] DISABLE_BY_FILES_OPTS = [ cfg.ListOpt('disable_by_file_paths', default=[], help='Check the presence of a file based on a port to ' 'determine if an application is running on a port. ' 'Expects a "port:path" list of strings. Used by ' 'DisableByFilesPortsHealthcheck plugin.'), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/healthcheck/pluginbase.py0000664000175000017500000000272300000000000024754 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc class HealthcheckResult(object): """Result of a ``healthcheck`` method call should be this object.""" def __init__(self, available, reason, details=None): self.available = available self.reason = reason self.details = details class HealthcheckBaseExtension(metaclass=abc.ABCMeta): def __init__(self, oslo_conf, conf): self.oslo_conf = oslo_conf self.conf = conf @abc.abstractmethod def healthcheck(self, server_port): """method called by the healthcheck middleware return: HealthcheckResult object """ def _conf_get(self, key, group='healthcheck'): if key in self.conf: # Validate value type self.oslo_conf.set_override(key, self.conf[key], group=group) return getattr(getattr(self.oslo_conf, group), key) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/http_proxy_to_wsgi.py0000664000175000017500000000635400000000000024337 0ustar00zuulzuul00000000000000# -*- encoding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. See the License for the specific language governing permissions and # limitations under the License. from oslo_config import cfg from oslo_middleware import base OPTS = [ cfg.BoolOpt('enable_proxy_headers_parsing', default=False, help="Whether the application is behind a proxy or not. " "This determines if the middleware should parse the " "headers or not.") ] class HTTPProxyToWSGI(base.ConfigurableMiddleware): """HTTP proxy to WSGI termination middleware. This middleware overloads WSGI environment variables with the one provided by the remote HTTP reverse proxy. """ def __init__(self, application, *args, **kwargs): super(HTTPProxyToWSGI, self).__init__(application, *args, **kwargs) self.oslo_conf.register_opts(OPTS, group='oslo_middleware') @staticmethod def _parse_rfc7239_header(header): """Parses RFC7239 Forward headers. e.g. for=192.0.2.60;proto=http, for=192.0.2.60;by=203.0.113.43 """ result = [] for proxy in header.split(","): entry = {} for d in proxy.split(";"): key, _, value = d.partition("=") entry[key.lower().strip()] = value.strip() result.append(entry) return result def process_request(self, req): if not self._conf_get('enable_proxy_headers_parsing'): return fwd_hdr = req.environ.get("HTTP_FORWARDED") if fwd_hdr: proxies = self._parse_rfc7239_header(fwd_hdr) # Let's use the value from the first proxy if proxies: proxy = proxies[0] forwarded_proto = proxy.get("proto") if forwarded_proto: req.environ['wsgi.url_scheme'] = forwarded_proto forwarded_host = proxy.get("host") if forwarded_host: req.environ['HTTP_HOST'] = forwarded_host forwarded_for = proxy.get("for") if forwarded_for: req.environ['REMOTE_ADDR'] = forwarded_for else: # World before RFC7239 forwarded_proto = req.environ.get("HTTP_X_FORWARDED_PROTO") if forwarded_proto: req.environ['wsgi.url_scheme'] = forwarded_proto forwarded_host = req.environ.get("HTTP_X_FORWARDED_HOST") if forwarded_host: req.environ['HTTP_HOST'] = forwarded_host forwarded_for = req.environ.get("HTTP_X_FORWARDED_FOR") if forwarded_for: req.environ['REMOTE_ADDR'] = forwarded_for v = req.environ.get("HTTP_X_FORWARDED_PREFIX") if v: req.environ['SCRIPT_NAME'] = v + req.environ['SCRIPT_NAME'] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708612004.429549 oslo.middleware-6.1.0/oslo_middleware/locale/0000775000175000017500000000000000000000000021241 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708612004.429549 oslo.middleware-6.1.0/oslo_middleware/locale/en_GB/0000775000175000017500000000000000000000000022213 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4415514 oslo.middleware-6.1.0/oslo_middleware/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000024000 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/locale/en_GB/LC_MESSAGES/oslo_middleware.po0000664000175000017500000000226000000000000027511 0ustar00zuulzuul00000000000000# Translations template for oslo.middleware. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the oslo.middleware # project. # # Translators: # Andi Chandler , 2014 # Andi Chandler , 2023. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.middleware VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2023-06-27 13:54+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2023-06-21 08:58+0000\n" "Last-Translator: Andi Chandler \n" "Language: en_GB\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: English (United Kingdom)\n" #, python-format msgid "Request is too large. Larger than %s" msgstr "Request is too large. Larger than %s" #, python-format msgid "Request is too large. Larger than %s." msgstr "Request is too large. Larger than %s." #, python-format msgid "Request is too large. Larger than max_request_body_size (%s)." msgstr "Request is too large. Larger than max_request_body_size (%s)." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/opts.py0000664000175000017500000001504200000000000021343 0ustar00zuulzuul00000000000000# Copyright 2014 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import itertools from oslo_middleware import basic_auth from oslo_middleware import cors from oslo_middleware.healthcheck import opts as healthcheck_opts from oslo_middleware import http_proxy_to_wsgi from oslo_middleware import sizelimit __all__ = [ 'list_opts', 'list_opts_sizelimit', 'list_opts_cors', 'list_opts_http_proxy_to_wsgi', 'list_opts_healthcheck', 'list_opts_basic_auth', ] def list_opts(): """Return a list of oslo.config options for ALL of the middleware classes. The returned list includes all oslo.config options which may be registered at runtime by the library. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'oslo.middleware' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by this library. :returns: a list of (group_name, opts) tuples """ return list( itertools.chain( list_opts_sizelimit(), list_opts_cors(), list_opts_http_proxy_to_wsgi(), list_opts_healthcheck(), list_opts_basic_auth(), ) ) def list_opts_sizelimit(): """Return a list of oslo.config options for the sizelimit middleware. The returned list includes all oslo.config options which may be registered at runtime by the library. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'oslo.middleware' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by this library. :returns: a list of (group_name, opts) tuples """ return [ ('oslo_middleware', copy.deepcopy(sizelimit._opts)), ] def list_opts_cors(): """Return a list of oslo.config options for the cors middleware. The returned list includes all oslo.config options which may be registered at runtime by the library. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'oslo.middleware' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by this library. :returns: a list of (group_name, opts) tuples """ return [ ('cors', copy.deepcopy(cors.CORS_OPTS)), ] def list_opts_http_proxy_to_wsgi(): """Return a list of oslo.config options for http_proxy_to_wsgi. The returned list includes all oslo.config options which may be registered at runtime by the library. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'oslo.middleware' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by this library. :returns: a list of (group_name, opts) tuples """ return [ ('oslo_middleware', copy.deepcopy(http_proxy_to_wsgi.OPTS)), ] def list_opts_healthcheck(): """Return a list of oslo.config options for healthcheck. The returned list includes all oslo.config options which may be registered at runtime by the library. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'oslo.middleware' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by this library. :returns: a list of (group_name, opts) tuples """ # standard opts and the most common plugin to turn up in sample config. # can figure out a better way of exposing plugin opts later if required. return [ ('healthcheck', copy.deepcopy(healthcheck_opts.HEALTHCHECK_OPTS + healthcheck_opts.DISABLE_BY_FILE_OPTS + healthcheck_opts.DISABLE_BY_FILES_OPTS)) ] def list_opts_basic_auth(): """Return a list of oslo.config options for basic auth middleware. The returned list includes all oslo.config options which may be registered at runtime by the library. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'oslo.middleware' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by this library. :returns: a list of (group_name, opts) tuples """ return [ ('oslo_middleware', copy.deepcopy(basic_auth.OPTS)), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/request_id.py0000664000175000017500000000442700000000000022527 0ustar00zuulzuul00000000000000# Copyright (c) 2013 NEC Corporation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re from oslo_context import context import webob.dec from oslo_middleware import base ENV_REQUEST_ID = 'openstack.request_id' GLOBAL_REQ_ID = 'openstack.global_request_id' HTTP_RESP_HEADER_REQUEST_ID = 'x-openstack-request-id' INBOUND_HEADER = 'X-Openstack-Request-Id' ID_FORMAT = (r'^req-[a-f0-9]{8}-[a-f0-9]{4}-' r'[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') class RequestId(base.ConfigurableMiddleware): """Middleware that ensures request ID. It ensures to assign request ID for each API request and set it to request environment. The request ID is also added to API response. """ # if compat_headers is set, we also return the request_id in those # headers as well. This allows projects like Nova to adopt # oslo.middleware without impacting existing users. compat_headers = [] def set_global_req_id(self, req): gr_id = req.headers.get(INBOUND_HEADER, "") if re.match(ID_FORMAT, gr_id): req.environ[GLOBAL_REQ_ID] = gr_id # TODO(sdague): it would be nice to warn if we dropped a bogus # request_id, but the infrastructure for doing that isn't yet # setup at this stage. @webob.dec.wsgify def __call__(self, req): self.set_global_req_id(req) req_id = context.generate_request_id() req.environ[ENV_REQUEST_ID] = req_id response = req.get_response(self.application) return_headers = [HTTP_RESP_HEADER_REQUEST_ID] return_headers.extend(self.compat_headers) for header in return_headers: if header not in response.headers: response.headers.add(header, req_id) return response ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/sizelimit.py0000664000175000017500000000660700000000000022376 0ustar00zuulzuul00000000000000# Copyright (c) 2012 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Request Body limiting middleware. """ import logging from oslo_config import cfg import webob.dec import webob.exc from oslo_middleware._i18n import _ from oslo_middleware import base LOG = logging.getLogger(__name__) _oldopts = [cfg.DeprecatedOpt('osapi_max_request_body_size', group='DEFAULT'), cfg.DeprecatedOpt('max_request_body_size', group='DEFAULT')] _opts = [ # default request size is 112k cfg.IntOpt('max_request_body_size', default=114688, help='The maximum body size for each ' ' request, in bytes.', deprecated_opts=_oldopts) ] class LimitingReader(object): """Reader to limit the size of an incoming request.""" def __init__(self, data, limit): """Initiates LimitingReader object. :param data: Underlying data object :param limit: maximum number of bytes the reader should allow """ self.data = data self.limit = limit self.bytes_read = 0 def __iter__(self): for chunk in self.data: self.bytes_read += len(chunk) if self.bytes_read > self.limit: msg = _("Request is too large. Larger than %s") % self.limit raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) else: yield chunk def read(self, i=None): # NOTE(jamielennox): We can't simply provide the default to the read() # call as the expected default differs between mod_wsgi and eventlet if i is None: result = self.data.read() else: result = self.data.read(i) self.bytes_read += len(result) if self.bytes_read > self.limit: msg = _("Request is too large. Larger than %s.") % self.limit raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) return result class RequestBodySizeLimiter(base.ConfigurableMiddleware): """Limit the size of incoming requests.""" def __init__(self, application, conf=None): super(RequestBodySizeLimiter, self).__init__(application, conf) self.oslo_conf.register_opts(_opts, group='oslo_middleware') @webob.dec.wsgify def __call__(self, req): max_size = self._conf_get('max_request_body_size') if (req.content_length is not None and req.content_length > max_size): msg = _("Request is too large. " "Larger than max_request_body_size (%s).") % max_size LOG.info(msg) raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) if req.content_length is None: limiter = LimitingReader(req.body_file, max_size) req.body_file = limiter return self.application ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/stats.py0000664000175000017500000001103600000000000021513 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Cisco Systems # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import re import statsd import webob.dec from oslo_middleware import base LOG = logging.getLogger(__name__) VERSION_REGEX = re.compile(r"/(v[0-9]{1}\.[0-9]{1})") UUID_REGEX = re.compile( r'.*(\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*a', re.IGNORECASE) # UUIDs without the - char, used in some places in Nova URLs. SHORT_UUID_REGEX = re.compile(r'.*(\.[0-9a-fA-F]{32}).*') class StatsMiddleware(base.ConfigurableMiddleware): """Send stats to statsd based on API requests. Examines the URL path and request method, and sends a stat count and timer to a statsd host based on the path/method. If your statsd is configured to send stats to Graphite, you'll end up with stat names of the form:: timer..... Note that URLs with versions in them (pretty much all of Openstack) are always processed to replace the dot with _, so for example v2.0 becomes v2_0, and v1.1 becomes v1_1, since a dot '.' has special meaning in Graphite. The original StatsD is written in nodejs. If you want a Python implementation, install Bucky instead as it's a drop-in replacement (and much nicer IMO). The Paste config must contain some parameters. Configure a filter like this:: [filter:stats] paste.filter_factory = oslo_middleware.stats:StatsMiddleware.factory name = my_application_name # e.g. 'glance' stats_host = my_statsd_host.example.com # Optional args to further process the stat name that's generated: remove_uuid = True remove_short_uuid = True # The above uuid processing is required in, e.g. Nova, if you want to # collect generic stats rather than one per server instance. """ def __init__(self, application, conf): super(StatsMiddleware, self).__init__(application, conf) self.application = application self.stat_name = conf.get('name') if self.stat_name is None: raise AttributeError('name must be specified') self.stats_host = conf.get('stats_host') if self.stats_host is None: raise AttributeError('stats_host must be specified') self.remove_uuid = conf.get('remove_uuid', False) self.remove_short_uuid = conf.get('remove_short_uuid', False) self.statsd = statsd.StatsClient(self.stats_host) @staticmethod def strip_short_uuid(path): """Remove short-form UUID from supplied path. Only call after replacing slashes with dots in path. """ match = SHORT_UUID_REGEX.match(path) if match is None: return path return path.replace(match.group(1), '') @staticmethod def strip_uuid(path): """Remove normal-form UUID from supplied path. Only call after replacing slashes with dots in path. """ match = UUID_REGEX.match(path) if match is None: return path return path.replace(match.group(1), '') @staticmethod def strip_dot_from_version(path): # Replace vN.N with vNN. match = VERSION_REGEX.match(path) if match is None: return path return path.replace(match.group(1), match.group(1).replace('.', '')) @webob.dec.wsgify def __call__(self, request): path = request.path path = self.strip_dot_from_version(path) # Remove leading slash, if any, so we can be sure of the number # of dots just below. path = path.lstrip('/') stat = "{name}.{method}".format( name=self.stat_name, method=request.method) if path != '': stat += '.' + path.replace('/', '.') if self.remove_short_uuid: stat = self.strip_short_uuid(stat) if self.remove_uuid: stat = self.strip_uuid(stat) LOG.debug("Incrementing stat count %s", stat) with self.statsd.timer(stat): return request.get_response(self.application) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4455523 oslo.middleware-6.1.0/oslo_middleware/tests/0000775000175000017500000000000000000000000021144 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/__init__.py0000664000175000017500000000000000000000000023243 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_auth_basic.py0000664000175000017500000001564700000000000024674 0ustar00zuulzuul00000000000000# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import os import tempfile from oslo_config import cfg import webob from oslo_middleware import basic_auth as auth from oslotest import base as test_base class TestAuthBasic(test_base.BaseTestCase): def setUp(self): super().setUp() @webob.dec.wsgify def fake_app(req): return webob.Response() self.fake_app = fake_app self.request = webob.Request.blank('/') def write_auth_file(self, data=None): if not data: data = '\n' with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(data) self.addCleanup(os.remove, f.name) return f.name def test_middleware_authenticate(self): auth_file = self.write_auth_file( 'myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.' 'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n') cfg.CONF.set_override('http_basic_auth_user_file', auth_file, group='oslo_middleware') self.middleware = auth.BasicAuthMiddleware(self.fake_app) self.request.environ[ 'HTTP_AUTHORIZATION'] = 'Basic bXlOYW1lOm15UGFzc3dvcmQ=' response = self.request.get_response(self.middleware) self.assertEqual('200 OK', response.status) def test_middleware_unauthenticated(self): auth_file = self.write_auth_file( 'myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.' 'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n') cfg.CONF.set_override('http_basic_auth_user_file', auth_file, group='oslo_middleware') self.middleware = auth.BasicAuthMiddleware(self.fake_app) response = self.request.get_response(self.middleware) self.assertEqual('401 Unauthorized', response.status) def test_authenticate(self): auth_file = self.write_auth_file( 'foo:bar\nmyName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.' 'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n') # test basic auth self.assertEqual( {'HTTP_X_USER': 'myName', 'HTTP_X_USER_NAME': 'myName'}, auth.authenticate( auth_file, 'myName', b'myPassword') ) # test failed auth e = self.assertRaises(webob.exc.HTTPBadRequest, auth.authenticate, auth_file, 'foo', b'bar') self.assertEqual('Only bcrypt digested ' 'passwords are supported for foo', str(e)) # test problem reading user data file auth_file = auth_file + '.missing' e = self.assertRaises(webob.exc.HTTPBadRequest, auth.authenticate, auth_file, 'myName', b'myPassword') self.assertEqual( 'Problem reading auth file', str(e)) def test_auth_entry(self): entry_pass = ('myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.' 'JETVCWBkc32C63UP2aYrGoYOEpbJm') entry_fail = 'foo:bar' # success self.assertEqual( {'HTTP_X_USER': 'myName', 'HTTP_X_USER_NAME': 'myName'}, auth.auth_entry(entry_pass, b'myPassword') ) # failed, unknown digest format ex = self.assertRaises(webob.exc.HTTPBadRequest, auth.auth_entry, entry_fail, b'bar') self.assertEqual('Only bcrypt digested ' 'passwords are supported for foo', str(ex)) # failed, incorrect password self.assertRaises(webob.exc.HTTPUnauthorized, auth.auth_entry, entry_pass, b'bar') def test_validate_auth_file(self): auth_file = self.write_auth_file( 'myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.' 'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n') # success, valid config auth.validate_auth_file(auth_file) # failed, missing auth file auth_file = auth_file + '.missing' self.assertRaises(auth.ConfigInvalid, auth.validate_auth_file, auth_file) # failed, invalid entry auth_file = self.write_auth_file( 'foo:bar\nmyName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.' 'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n') self.assertRaises(webob.exc.HTTPBadRequest, auth.validate_auth_file, auth_file) def test_parse_token(self): # success with bytes token = base64.b64encode(b'myName:myPassword') self.assertEqual( ('myName', b'myPassword'), auth.parse_token(token) ) # success with string token = str(token, encoding='utf-8') self.assertEqual( ('myName', b'myPassword'), auth.parse_token(token) ) # failed, invalid base64 e = self.assertRaises(webob.exc.HTTPBadRequest, auth.parse_token, token[:-1]) self.assertEqual('Could not decode authorization token', str(e)) # failed, no colon in token token = str(base64.b64encode(b'myNamemyPassword'), encoding='utf-8') e = self.assertRaises(webob.exc.HTTPBadRequest, auth.parse_token, token[:-1]) self.assertEqual('Could not decode authorization token', str(e)) def test_parse_header(self): auth_value = 'Basic bXlOYW1lOm15UGFzc3dvcmQ=' # success self.assertEqual( 'bXlOYW1lOm15UGFzc3dvcmQ=', auth.parse_header({ 'HTTP_AUTHORIZATION': auth_value }) ) # failed, missing Authorization header e = self.assertRaises(webob.exc.HTTPUnauthorized, auth.parse_header, {}) # failed missing token e = self.assertRaises(webob.exc.HTTPBadRequest, auth.parse_header, {'HTTP_AUTHORIZATION': 'Basic'}) self.assertEqual('Could not parse Authorization header', str(e)) # failed, type other than Basic digest_value = 'Digest username="myName" nonce="foobar"' e = self.assertRaises(webob.exc.HTTPBadRequest, auth.parse_header, {'HTTP_AUTHORIZATION': digest_value}) self.assertEqual('Unsupported authorization type "Digest"', str(e)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_base.py0000664000175000017500000000627500000000000023501 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import webob from oslo_middleware.base import ConfigurableMiddleware from oslo_middleware.base import Middleware from oslotest.base import BaseTestCase @webob.dec.wsgify def application(req): return 'Hello, World!!!' class TestBase(BaseTestCase): """Test the base middleware class.""" def test_extend_with_request(self): """Assert that a newer middleware behaves as appropriate. This tests makes sure that the request is passed to the middleware's implementation. """ # Bootstrap the application self.application = RequestBase(application) # Send a request through. request = webob.Request({}, method='GET') request.get_response(self.application) self.assertTrue(self.application.called_with_request) def test_extend_without_request(self): """Assert that an older middleware behaves as appropriate. This tests makes sure that the request method is NOT passed to the middleware's implementation, and that there are no other expected errors. """ # Bootstrap the application self.application = NoRequestBase(application) # Send a request through. request = webob.Request({}, method='GET') request.get_response(self.application) self.assertTrue(self.application.called_without_request) def test_no_content_type_added(self): class TestMiddleware(Middleware): @staticmethod def process_request(req): return "foobar" m = TestMiddleware(None) request = webob.Request({}, method='GET') response = request.get_response(m) self.assertNotIn('Content-Type', response.headers) def test_paste_deploy_legacy(self): app = LegacyMiddlewareTest.factory( {'global': True}, local=True)(application) self.assertEqual({}, app.conf) def test_paste_deploy_configurable(self): app = ConfigurableMiddlewareTest.factory( {'global': True}, local=True)(application) self.assertEqual({'global': True, 'local': True}, app.conf) class NoRequestBase(Middleware): """Test middleware, implements old model.""" def process_response(self, response): self.called_without_request = True return response class RequestBase(Middleware): """Test middleware, implements new model.""" def process_response(self, response, request): self.called_with_request = True return response class ConfigurableMiddlewareTest(ConfigurableMiddleware): pass class LegacyMiddlewareTest(Middleware): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_catch_errors.py0000664000175000017500000000534700000000000025244 0ustar00zuulzuul00000000000000# Copyright (c) 2013 NEC Corporation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from oslotest import base as test_base import webob.dec import webob.exc from oslo_middleware import catch_errors class CatchErrorsTest(test_base.BaseTestCase): def _test_has_request_id(self, application, expected_code=None): app = catch_errors.CatchErrors(application) req = webob.Request.blank('/test') req.environ['HTTP_X_AUTH_TOKEN'] = 'hello=world' res = req.get_response(app) self.assertEqual(expected_code, res.status_int) def test_success_response(self): @webob.dec.wsgify def application(req): return 'Hello, World!!!' self._test_has_request_id(application, webob.exc.HTTPOk.code) def test_internal_server_error(self): @webob.dec.wsgify def application(req): raise Exception() with mock.patch.object(catch_errors.LOG, 'exception') as log_exc: self._test_has_request_id(application, webob.exc.HTTPInternalServerError.code) self.assertEqual(1, log_exc.call_count) req_log = log_exc.call_args[0][1] self.assertIn('X-Auth-Token: *****', str(req_log)) def test_filter_tokens_from_log(self): logger = self.useFixture(fixtures.FakeLogger(nuke_handlers=False)) @webob.dec.wsgify def application(req): raise Exception() app = catch_errors.CatchErrors(application) req = webob.Request.blank('/test', text='test data', method='POST', headers={'X-Auth-Token': 'secret1', 'X-Service-Token': 'secret2', 'X-Other-Token': 'secret3'}) res = req.get_response(app) self.assertEqual(500, res.status_int) output = logger.output self.assertIn('X-Auth-Token: *****', output) self.assertIn('X-Service-Token: *****', output) self.assertIn('X-Other-Token: *****', output) self.assertIn('test data', output) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_correlation_id.py0000664000175000017500000000316300000000000025555 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Rackspace Hosting # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from oslotest import base as test_base from oslo_middleware import correlation_id class CorrelationIdTest(test_base.BaseTestCase): def setUp(self): super(CorrelationIdTest, self).setUp() def test_process_request(self): app = mock.Mock() req = mock.Mock() req.headers = {} mock_uuid4 = mock.Mock() mock_uuid4.return_value = "fake_uuid" self.useFixture(fixtures.MockPatch('uuid.uuid4', mock_uuid4)) middleware = correlation_id.CorrelationId(app) middleware(req) self.assertEqual("fake_uuid", req.headers.get("X_CORRELATION_ID")) def test_process_request_should_not_regenerate_correlation_id(self): app = mock.Mock() req = mock.Mock() req.headers = {"X_CORRELATION_ID": "correlation_id"} middleware = correlation_id.CorrelationId(app) middleware(req) self.assertEqual("correlation_id", req.headers.get("X_CORRELATION_ID")) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_cors.py0000664000175000017500000015423600000000000023536 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_config import fixture from oslotest import base as test_base import webob import webob.dec import webob.exc as exc from oslo_middleware import cors @webob.dec.wsgify def test_application(req): if req.path_info == '/server_cors': # Mirror back the origin in the request. response = webob.Response(status=200) response.headers['Access-Control-Allow-Origin'] = \ req.headers['Origin'] response.headers['X-Server-Generated-Response'] = '1' return response if req.path_info == '/server_cors_vary': # Mirror back the origin in the request. response = webob.Response(status=200) response.headers['Vary'] = 'Custom-Vary' return response if req.path_info == '/server_no_cors': # Send a response with no CORS headers. response = webob.Response(status=200) return response if req.method == 'OPTIONS': raise exc.HTTPNotFound() return 'Hello World' class CORSTestBase(test_base.BaseTestCase): """Base class for all CORS tests. Sets up applications and helper methods. """ def setUp(self): """Setup the tests.""" super(CORSTestBase, self).setUp() # Set up the config fixture. self.config_fixture = self.useFixture(fixture.Config()) self.config = self.config_fixture.conf def assertCORSResponse(self, response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, vary='Origin', has_content_type=False): """Test helper for CORS response headers. Assert all the headers in a given response. By default, we assume the response is empty. """ # Assert response status. self.assertEqual(status, response.status) # Assert the Access-Control-Allow-Origin header. self.assertHeader(response, 'Access-Control-Allow-Origin', allow_origin) # Assert the Access-Control-Max-Age header. self.assertHeader(response, 'Access-Control-Max-Age', max_age) # Assert the Access-Control-Allow-Methods header. self.assertHeader(response, 'Access-Control-Allow-Methods', allow_methods) # Assert the Access-Control-Allow-Headers header. self.assertHeader(response, 'Access-Control-Allow-Headers', allow_headers) # Assert the Access-Control-Allow-Credentials header. self.assertHeader(response, 'Access-Control-Allow-Credentials', allow_credentials) # Assert the Access-Control-Expose-Headers header. self.assertHeader(response, 'Access-Control-Expose-Headers', expose_headers) # Assert no Content-Type added. if not has_content_type: self.assertHeader(response, 'Content-Type') # If we're expecting an origin response, also assert that the # Vary: Origin header is set, since this implementation of the CORS # specification permits multiple origin domains. if allow_origin: self.assertHeader(response, 'Vary', vary) def assertHeader(self, response, header, value=None): if value: self.assertIn(header, response.headers) self.assertEqual(str(value), response.headers[header]) else: self.assertNotIn(header, response.headers) class CORSTestDefaultOverrides(CORSTestBase): def setUp(self): super(CORSTestDefaultOverrides, self).setUp() fixture = self.config_fixture # Line length accommodation fixture.load_raw_values(group='cors', allowed_origin='http://valid.example.com') fixture.load_raw_values(group='cors.override_creds', allowed_origin='http://creds.example.com', allow_credentials='True') fixture.load_raw_values(group='cors.override_headers', allowed_origin='http://headers.example.com', expose_headers='X-Header-1,X-Header-2', allow_headers='X-Header-1,X-Header-2') self.override_opts = { 'expose_headers': ['X-Header-1'], 'allow_headers': ['X-Header-2'], 'allow_methods': ['GET', 'DELETE'], 'allow_credentials': False, 'max_age': 10 } def test_config_defaults(self): """Assert that using set_defaults overrides the appropriate values.""" cors.set_defaults(**self.override_opts) for opt in cors.CORS_OPTS: if opt.dest in self.override_opts: self.assertEqual(self.override_opts[opt.dest], opt.default) def test_invalid_default_option(self): """Assert that using set_defaults only permits valid options.""" self.assertRaises(AttributeError, cors.set_defaults, allowed_origin='test') def test_cascading_override(self): """Assert that using set_defaults overrides cors.* config values.""" # set defaults cors.set_defaults(**self.override_opts) # Now that the config is set up, create our application. self.application = cors.CORS(test_application, self.config) # Check the global configuration for expected values: gc = self.config.cors self.assertEqual(['http://valid.example.com'], gc.allowed_origin) self.assertEqual(self.override_opts['allow_credentials'], gc.allow_credentials) self.assertEqual(self.override_opts['expose_headers'], gc.expose_headers) self.assertEqual(10, gc.max_age) self.assertEqual(self.override_opts['allow_methods'], gc.allow_methods) self.assertEqual(self.override_opts['allow_headers'], gc.allow_headers) # Check the child configuration for expected values: cc = self.config['cors.override_creds'] self.assertEqual(['http://creds.example.com'], cc.allowed_origin) self.assertTrue(cc.allow_credentials) self.assertEqual(self.override_opts['expose_headers'], cc.expose_headers) self.assertEqual(10, cc.max_age) self.assertEqual(self.override_opts['allow_methods'], cc.allow_methods) self.assertEqual(self.override_opts['allow_headers'], cc.allow_headers) # Check the other child configuration for expected values: ec = self.config['cors.override_headers'] self.assertEqual(['http://headers.example.com'], ec.allowed_origin) self.assertEqual(self.override_opts['allow_credentials'], ec.allow_credentials) self.assertEqual(['X-Header-1', 'X-Header-2'], ec.expose_headers) self.assertEqual(10, ec.max_age) self.assertEqual(self.override_opts['allow_methods'], ec.allow_methods) self.assertEqual(['X-Header-1', 'X-Header-2'], ec.allow_headers) class CORSTestFilterFactory(CORSTestBase): """Test the CORS filter_factory method.""" def test_filter_factory(self): self.config([]) # Test a valid filter. filter = cors.filter_factory(None, allowed_origin='http://valid.example.com', allow_credentials='False', max_age='', expose_headers='', allow_methods='GET', allow_headers='') application = filter(test_application) self.assertIn('http://valid.example.com', application.allowed_origins) config = application.allowed_origins['http://valid.example.com'] self.assertEqual(False, config['allow_credentials']) self.assertIsNone(config['max_age']) self.assertEqual([], config['expose_headers']) self.assertEqual(['GET'], config['allow_methods']) self.assertEqual([], config['allow_headers']) def test_filter_factory_multiorigin(self): self.config([]) # Test a valid filter. filter = cors.filter_factory(None, allowed_origin='http://valid.example.com,' 'http://other.example.com') application = filter(test_application) self.assertIn('http://valid.example.com', application.allowed_origins) self.assertIn('http://other.example.com', application.allowed_origins) def test_no_origin_fail(self): '''Assert that a filter factory with no allowed_origin fails.''' self.assertRaises(TypeError, cors.filter_factory, global_conf=None, # allowed_origin=None, # Expected value. allow_credentials='False', max_age='', expose_headers='', allow_methods='GET', allow_headers='') def test_no_origin_but_oslo_config_project(self): '''Assert that a filter factory with oslo_config_project succeed.''' cors.filter_factory(global_conf=None, oslo_config_project='foobar') def test_cor_config_sections_with_defaults(self): '''Assert cors.* config sections with default values work.''' # Set up the config fixture. self.config_fixture.load_raw_values(group='cors.subdomain') # Now that the config is set up, create our application. self.application = cors.CORS(test_application, self.config) class CORSRegularRequestTest(CORSTestBase): """CORS Specification Section 6.1 http://www.w3.org/TR/cors/#resource-requests """ # List of HTTP methods (other than OPTIONS) to test with. methods = ['POST', 'PUT', 'DELETE', 'GET', 'TRACE', 'HEAD'] def setUp(self): """Setup the tests.""" super(CORSRegularRequestTest, self).setUp() fixture = self.config_fixture # Line length accommodation fixture.load_raw_values(group='cors', allowed_origin='http://valid.example.com', allow_credentials='False', max_age='', expose_headers='', allow_methods='GET', allow_headers='') fixture.load_raw_values(group='cors.credentials', allowed_origin='http://creds.example.com', allow_credentials='True') fixture.load_raw_values(group='cors.exposed-headers', allowed_origin='http://headers.example.com', expose_headers='X-Header-1,X-Header-2', allow_headers='X-Header-1,X-Header-2') fixture.load_raw_values(group='cors.cached', allowed_origin='http://cached.example.com', max_age='3600') fixture.load_raw_values(group='cors.get-only', allowed_origin='http://get.example.com', allow_methods='GET') fixture.load_raw_values(group='cors.all-methods', allowed_origin='http://all.example.com', allow_methods='GET,PUT,POST,DELETE,HEAD') fixture.load_raw_values(group='cors.duplicate', allowed_origin='http://domain1.example.com,' 'http://domain2.example.com') # Now that the config is set up, create our application. self.application = cors.CORS(test_application, self.config) def test_config_overrides(self): """Assert that the configuration options are properly registered.""" # Confirm global configuration gc = self.config.cors self.assertEqual(['http://valid.example.com'], gc.allowed_origin) self.assertEqual(False, gc.allow_credentials) self.assertEqual([], gc.expose_headers) self.assertIsNone(gc.max_age) self.assertEqual(['GET'], gc.allow_methods) self.assertEqual([], gc.allow_headers) # Confirm credentials overrides. cc = self.config['cors.credentials'] self.assertEqual(['http://creds.example.com'], cc.allowed_origin) self.assertEqual(True, cc.allow_credentials) self.assertEqual(gc.expose_headers, cc.expose_headers) self.assertEqual(gc.max_age, cc.max_age) self.assertEqual(gc.allow_methods, cc.allow_methods) self.assertEqual(gc.allow_headers, cc.allow_headers) # Confirm exposed-headers overrides. ec = self.config['cors.exposed-headers'] self.assertEqual(['http://headers.example.com'], ec.allowed_origin) self.assertEqual(gc.allow_credentials, ec.allow_credentials) self.assertEqual(['X-Header-1', 'X-Header-2'], ec.expose_headers) self.assertEqual(gc.max_age, ec.max_age) self.assertEqual(gc.allow_methods, ec.allow_methods) self.assertEqual(['X-Header-1', 'X-Header-2'], ec.allow_headers) # Confirm cached overrides. chc = self.config['cors.cached'] self.assertEqual(['http://cached.example.com'], chc.allowed_origin) self.assertEqual(gc.allow_credentials, chc.allow_credentials) self.assertEqual(gc.expose_headers, chc.expose_headers) self.assertEqual(3600, chc.max_age) self.assertEqual(gc.allow_methods, chc.allow_methods) self.assertEqual(gc.allow_headers, chc.allow_headers) # Confirm get-only overrides. goc = self.config['cors.get-only'] self.assertEqual(['http://get.example.com'], goc.allowed_origin) self.assertEqual(gc.allow_credentials, goc.allow_credentials) self.assertEqual(gc.expose_headers, goc.expose_headers) self.assertEqual(gc.max_age, goc.max_age) self.assertEqual(['GET'], goc.allow_methods) self.assertEqual(gc.allow_headers, goc.allow_headers) # Confirm all-methods overrides. ac = self.config['cors.all-methods'] self.assertEqual(['http://all.example.com'], ac.allowed_origin) self.assertEqual(gc.allow_credentials, ac.allow_credentials) self.assertEqual(gc.expose_headers, ac.expose_headers) self.assertEqual(gc.max_age, ac.max_age) self.assertEqual(['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], ac.allow_methods) self.assertEqual(gc.allow_headers, ac.allow_headers) # Confirm duplicate domains. ac = self.config['cors.duplicate'] self.assertEqual(['http://domain1.example.com', 'http://domain2.example.com'], ac.allowed_origin) self.assertEqual(gc.allow_credentials, ac.allow_credentials) self.assertEqual(gc.expose_headers, ac.expose_headers) self.assertEqual(gc.max_age, ac.max_age) self.assertEqual(gc.allow_methods, ac.allow_methods) self.assertEqual(gc.allow_headers, ac.allow_headers) def test_no_origin_header(self): """CORS Specification Section 6.1.1 If the Origin header is not present terminate this set of steps. The request is outside the scope of this specification. """ for method in self.methods: request = webob.Request.blank('/') response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) def test_origin_headers(self): """CORS Specification Section 6.1.2 If the value of the Origin header is not a case-sensitive match for any of the values in list of origins, do not set any additional headers and terminate this set of steps. """ # Test valid origin header. for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://valid.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://valid.example.com', max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) # Test origin header not present in configuration. for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://invalid.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) # Test valid, but case-mismatched origin header. for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://VALID.EXAMPLE.COM' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) # Test valid header from list of duplicates. for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://domain2.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://domain2.example.com', max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) def test_supports_credentials(self): """CORS Specification Section 6.1.3 If the resource supports credentials add a single Access-Control-Allow-Origin header, with the value of the Origin header as value, and add a single Access-Control-Allow-Credentials header with the case-sensitive string "true" as value. Otherwise, add a single Access-Control-Allow-Origin header, with either the value of the Origin header or the string "*" as value. NOTE: We never use the "*" as origin. """ # Test valid origin header without credentials. for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://valid.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://valid.example.com', max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) # Test valid origin header with credentials for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://creds.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://creds.example.com', max_age=None, allow_methods=None, allow_headers=None, allow_credentials="true", expose_headers=None, has_content_type=True) def test_expose_headers(self): """CORS Specification Section 6.1.4 If the list of exposed headers is not empty add one or more Access-Control-Expose-Headers headers, with as values the header field names given in the list of exposed headers. """ for method in self.methods: request = webob.Request.blank('/') request.method = method request.headers['Origin'] = 'http://headers.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://headers.example.com', max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers='X-Header-1,X-Header-2', has_content_type=True) def test_application_options_response(self): """Assert that an application provided OPTIONS response is honored. If the underlying application, via middleware or other, provides a CORS response, its response should be honored. """ test_origin = 'http://creds.example.com' request = webob.Request.blank('/server_cors') request.method = "GET" request.headers['Origin'] = test_origin request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) # If the regular CORS handling catches this request, it should set # the allow credentials header. This makes sure that it doesn't. self.assertNotIn('Access-Control-Allow-Credentials', response.headers) self.assertEqual(response.headers['Access-Control-Allow-Origin'], test_origin) self.assertEqual(response.headers['X-Server-Generated-Response'], '1') def test_application_vary_respected(self): """Assert that an application's provided Vary header is persisted. If the underlying application, via middleware or other, provides a Vary header, its response should be honored. """ request = webob.Request.blank('/server_cors_vary') request.method = "GET" request.headers['Origin'] = 'http://valid.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://valid.example.com', max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None, vary='Custom-Vary,Origin', has_content_type=True) class CORSPreflightRequestTest(CORSTestBase): """CORS Specification Section 6.2 http://www.w3.org/TR/cors/#resource-preflight-requests """ def setUp(self): super(CORSPreflightRequestTest, self).setUp() fixture = self.config_fixture # Line length accommodation fixture.load_raw_values(group='cors', allowed_origin='http://valid.example.com', allow_credentials='False', max_age='', expose_headers='', allow_methods='GET', allow_headers='') fixture.load_raw_values(group='cors.credentials', allowed_origin='http://creds.example.com', allow_credentials='True') fixture.load_raw_values(group='cors.exposed-headers', allowed_origin='http://headers.example.com', expose_headers='X-Header-1,X-Header-2', allow_headers='X-Header-1,X-Header-2') fixture.load_raw_values(group='cors.cached', allowed_origin='http://cached.example.com', max_age='3600') fixture.load_raw_values(group='cors.get-only', allowed_origin='http://get.example.com', allow_methods='GET') fixture.load_raw_values(group='cors.all-methods', allowed_origin='http://all.example.com', allow_methods='GET,PUT,POST,DELETE,HEAD') # Now that the config is set up, create our application. self.application = cors.CORS(test_application, self.config) def test_config_overrides(self): """Assert that the configuration options are properly registered.""" # Confirm global configuration gc = self.config.cors self.assertEqual(gc.allowed_origin, ['http://valid.example.com']) self.assertEqual(gc.allow_credentials, False) self.assertEqual(gc.expose_headers, []) self.assertIsNone(gc.max_age) self.assertEqual(gc.allow_methods, ['GET']) self.assertEqual(gc.allow_headers, []) # Confirm credentials overrides. cc = self.config['cors.credentials'] self.assertEqual(['http://creds.example.com'], cc.allowed_origin) self.assertEqual(True, cc.allow_credentials) self.assertEqual(gc.expose_headers, cc.expose_headers) self.assertEqual(gc.max_age, cc.max_age) self.assertEqual(gc.allow_methods, cc.allow_methods) self.assertEqual(gc.allow_headers, cc.allow_headers) # Confirm exposed-headers overrides. ec = self.config['cors.exposed-headers'] self.assertEqual(['http://headers.example.com'], ec.allowed_origin) self.assertEqual(gc.allow_credentials, ec.allow_credentials) self.assertEqual(['X-Header-1', 'X-Header-2'], ec.expose_headers) self.assertEqual(gc.max_age, ec.max_age) self.assertEqual(gc.allow_methods, ec.allow_methods) self.assertEqual(['X-Header-1', 'X-Header-2'], ec.allow_headers) # Confirm cached overrides. chc = self.config['cors.cached'] self.assertEqual(['http://cached.example.com'], chc.allowed_origin) self.assertEqual(gc.allow_credentials, chc.allow_credentials) self.assertEqual(gc.expose_headers, chc.expose_headers) self.assertEqual(3600, chc.max_age) self.assertEqual(gc.allow_methods, chc.allow_methods) self.assertEqual(gc.allow_headers, chc.allow_headers) # Confirm get-only overrides. goc = self.config['cors.get-only'] self.assertEqual(['http://get.example.com'], goc.allowed_origin) self.assertEqual(gc.allow_credentials, goc.allow_credentials) self.assertEqual(gc.expose_headers, goc.expose_headers) self.assertEqual(gc.max_age, goc.max_age) self.assertEqual(['GET'], goc.allow_methods) self.assertEqual(gc.allow_headers, goc.allow_headers) # Confirm all-methods overrides. ac = self.config['cors.all-methods'] self.assertEqual(['http://all.example.com'], ac.allowed_origin) self.assertEqual(gc.allow_credentials, ac.allow_credentials) self.assertEqual(gc.expose_headers, ac.expose_headers) self.assertEqual(gc.max_age, ac.max_age) self.assertEqual(ac.allow_methods, ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']) self.assertEqual(gc.allow_headers, ac.allow_headers) def test_no_origin_header(self): """CORS Specification Section 6.2.1 If the Origin header is not present terminate this set of steps. The request is outside the scope of this specification. """ request = webob.Request.blank('/') request.method = "OPTIONS" response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_case_sensitive_origin(self): """CORS Specification Section 6.2.2 If the value of the Origin header is not a case-sensitive match for any of the values in list of origins do not set any additional headers and terminate this set of steps. """ # Test valid domain request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://valid.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://valid.example.com', max_age=None, allow_methods='GET', allow_headers='', allow_credentials=None, expose_headers=None) # Test invalid domain request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://invalid.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) # Test case-sensitive mismatch domain request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://VALID.EXAMPLE.COM' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_simple_header_response(self): """CORS Specification Section 3 A header is said to be a simple header if the header field name is an ASCII case-insensitive match for Accept, Accept-Language, or Content-Language or if it is an ASCII case-insensitive match for Content-Type and the header field value media type (excluding parameters) is an ASCII case-insensitive match for application/x-www-form-urlencoded, multipart/form-data, or text/plain. NOTE: We are not testing the media type cases. """ simple_headers = ','.join([ 'accept', 'accept-language', 'content-language', 'content-type' ]) request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://valid.example.com' request.headers['Access-Control-Request-Method'] = 'GET' request.headers['Access-Control-Request-Headers'] = simple_headers response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://valid.example.com', max_age=None, allow_methods='GET', allow_headers=simple_headers, allow_credentials=None, expose_headers=None) def test_no_request_method(self): """CORS Specification Section 6.2.3 If there is no Access-Control-Request-Method header or if parsing failed, do not set any additional headers and terminate this set of steps. The request is outside the scope of this specification. """ # Test valid domain, valid method. request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://get.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://get.example.com', max_age=None, allow_methods='GET', allow_headers=None, allow_credentials=None, expose_headers=None) # Test valid domain, invalid HTTP method. request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://valid.example.com' request.headers['Access-Control-Request-Method'] = 'TEAPOT' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) # Test valid domain, no HTTP method. request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://valid.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_invalid_method(self): """CORS Specification Section 6.2.3 If method is not a case-sensitive match for any of the values in list of methods do not set any additional headers and terminate this set of steps. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://get.example.com' request.headers['Access-Control-Request-Method'] = 'get' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_no_parse_request_headers(self): """CORS Specification Section 6.2.4 If there are no Access-Control-Request-Headers headers let header field-names be the empty list. If parsing failed do not set any additional headers and terminate this set of steps. The request is outside the scope of this specification. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://headers.example.com' request.headers['Access-Control-Request-Method'] = 'GET' request.headers['Access-Control-Request-Headers'] = 'value with spaces' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_no_request_headers(self): """CORS Specification Section 6.2.4 If there are no Access-Control-Request-Headers headers let header field-names be the empty list. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://headers.example.com' request.headers['Access-Control-Request-Method'] = 'GET' request.headers['Access-Control-Request-Headers'] = '' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://headers.example.com', max_age=None, allow_methods='GET', allow_headers=None, allow_credentials=None, expose_headers=None) def test_request_headers(self): """CORS Specification Section 6.2.4 Let header field-names be the values as result of parsing the Access-Control-Request-Headers headers. If there are no Access-Control-Request-Headers headers let header field-names be the empty list. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://headers.example.com' request.headers['Access-Control-Request-Method'] = 'GET' request.headers['Access-Control-Request-Headers'] = 'X-Header-1,' \ 'X-Header-2' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://headers.example.com', max_age=None, allow_methods='GET', allow_headers='X-Header-1,X-Header-2', allow_credentials=None, expose_headers=None) def test_request_headers_not_permitted(self): """CORS Specification Section 6.2.4, 6.2.6 If there are no Access-Control-Request-Headers headers let header field-names be the empty list. If any of the header field-names is not a ASCII case-insensitive match for any of the values in list of headers do not set any additional headers and terminate this set of steps. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://headers.example.com' request.headers['Access-Control-Request-Method'] = 'GET' request.headers['Access-Control-Request-Headers'] = 'X-Not-Exposed,' \ 'X-Never-Exposed' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_credentials(self): """CORS Specification Section 6.2.7 If the resource supports credentials add a single Access-Control-Allow-Origin header, with the value of the Origin header as value, and add a single Access-Control-Allow-Credentials header with the case-sensitive string "true" as value. Otherwise, add a single Access-Control-Allow-Origin header, with either the value of the Origin header or the string "*" as value. NOTE: We never use the "*" as origin. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://creds.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://creds.example.com', max_age=None, allow_methods='GET', allow_headers=None, allow_credentials="true", expose_headers=None) def test_optional_max_age(self): """CORS Specification Section 6.2.8 Optionally add a single Access-Control-Max-Age header with as value the amount of seconds the user agent is allowed to cache the result of the request. """ request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://cached.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://cached.example.com', max_age=3600, allow_methods='GET', allow_headers=None, allow_credentials=None, expose_headers=None) def test_allow_methods(self): """CORS Specification Section 6.2.9 Add one or more Access-Control-Allow-Methods headers consisting of (a subset of) the list of methods. Since the list of methods can be unbounded, simply returning the method indicated by Access-Control-Request-Method (if supported) can be enough. """ for method in ['GET', 'PUT', 'POST', 'DELETE']: request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://all.example.com' request.headers['Access-Control-Request-Method'] = method response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://all.example.com', max_age=None, allow_methods=method, allow_headers=None, allow_credentials=None, expose_headers=None) for method in ['PUT', 'POST', 'DELETE']: request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://get.example.com' request.headers['Access-Control-Request-Method'] = method response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin=None, max_age=None, allow_methods=None, allow_headers=None, allow_credentials=None, expose_headers=None) def test_allow_headers(self): """CORS Specification Section 6.2.10 Add one or more Access-Control-Allow-Headers headers consisting of (a subset of) the list of headers. If each of the header field-names is a simple header and none is Content-Type, this step may be skipped. If a header field name is a simple header and is not Content-Type, it is not required to be listed. Content-Type is to be listed as only a subset of its values makes it qualify as simple header. """ requested_headers = 'Content-Type,X-Header-1,Cache-Control,Expires,' \ 'Last-Modified,Pragma' request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://headers.example.com' request.headers['Access-Control-Request-Method'] = 'GET' request.headers['Access-Control-Request-Headers'] = requested_headers response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://headers.example.com', max_age=None, allow_methods='GET', allow_headers=requested_headers, allow_credentials=None, expose_headers=None) def test_application_options_response(self): """Assert that an application provided OPTIONS response is honored. If the underlying application, via middleware or other, provides a CORS response, its response should be honored. """ test_origin = 'http://creds.example.com' request = webob.Request.blank('/server_cors') request.method = "OPTIONS" request.headers['Origin'] = test_origin request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) # If the regular CORS handling catches this request, it should set # the allow credentials header. This makes sure that it doesn't. self.assertNotIn('Access-Control-Allow-Credentials', response.headers) self.assertEqual(test_origin, response.headers['Access-Control-Allow-Origin']) self.assertEqual('1', response.headers['X-Server-Generated-Response']) # If the application returns an OPTIONS response without CORS # headers, assert that we apply headers. request = webob.Request.blank('/server_no_cors') request.method = "OPTIONS" request.headers['Origin'] = 'http://get.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://get.example.com', max_age=None, allow_methods='GET', allow_headers=None, allow_credentials=None, expose_headers=None, has_content_type=True) class CORSTestWildcard(CORSTestBase): """Test the CORS wildcard specification.""" def setUp(self): super(CORSTestWildcard, self).setUp() fixture = self.config_fixture # Line length accommodation fixture.load_raw_values(group='cors', allowed_origin='http://default.example.com', allow_credentials='True', max_age='', expose_headers='', allow_methods='GET,PUT,POST,DELETE,HEAD', allow_headers='') fixture.load_raw_values(group='cors.wildcard', allowed_origin='*', allow_methods='GET') # Now that the config is set up, create our application. self.application = cors.CORS(test_application, self.config) def test_config_overrides(self): """Assert that the configuration options are properly registered.""" # Confirm global configuration gc = self.config.cors self.assertEqual(['http://default.example.com'], gc.allowed_origin) self.assertEqual(True, gc.allow_credentials) self.assertEqual([], gc.expose_headers) self.assertIsNone(gc.max_age) self.assertEqual(['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], gc.allow_methods) self.assertEqual([], gc.allow_headers) # Confirm all-methods overrides. ac = self.config['cors.wildcard'] self.assertEqual(['*'], ac.allowed_origin) self.assertEqual(True, gc.allow_credentials) self.assertEqual(gc.expose_headers, ac.expose_headers) self.assertEqual(gc.max_age, ac.max_age) self.assertEqual(['GET'], ac.allow_methods) self.assertEqual(gc.allow_headers, ac.allow_headers) def test_wildcard_domain(self): """CORS Specification, Wildcards If the configuration file specifies CORS settings for the wildcard '*' domain, it should return those for all origin domains except for the overrides. """ # Test valid domain request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://default.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://default.example.com', max_age=None, allow_methods='GET', allow_headers='', allow_credentials='true', expose_headers=None) # Test valid domain request = webob.Request.blank('/') request.method = "GET" request.headers['Origin'] = 'http://default.example.com' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='http://default.example.com', max_age=None, allow_headers='', allow_credentials='true', expose_headers=None, has_content_type=True) # Test invalid domain request = webob.Request.blank('/') request.method = "OPTIONS" request.headers['Origin'] = 'http://invalid.example.com' request.headers['Access-Control-Request-Method'] = 'GET' response = request.get_response(self.application) self.assertCORSResponse(response, status='200 OK', allow_origin='*', max_age=None, allow_methods='GET', allow_headers='', allow_credentials='true', expose_headers=None, has_content_type=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_entry_points.py0000664000175000017500000000253200000000000025314 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslotest import base import stevedore from testtools import matchers class TestPasteDeploymentEntryPoints(base.BaseTestCase): def test_entry_points(self): factory_classes = { 'catch_errors': 'CatchErrors', 'correlation_id': 'CorrelationId', 'cors': 'CORS', 'debug': 'Debug', 'healthcheck': 'Healthcheck', 'http_proxy_to_wsgi': 'HTTPProxyToWSGI', 'request_id': 'RequestId', 'sizelimit': 'RequestBodySizeLimiter', } em = stevedore.ExtensionManager('paste.filter_factory') # Ensure all the factories are defined by their names factory_names = [extension.name for extension in em] self.assertThat(factory_names, matchers.ContainsAll(factory_classes)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_healthcheck.py0000664000175000017500000002441300000000000025024 0ustar00zuulzuul00000000000000# Copyright (c) 2013 NEC Corporation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import threading import time from unittest import mock from oslo_config import fixture as config from oslo_serialization import jsonutils from oslotest import base as test_base import requests import webob.dec import webob.exc from oslo_middleware import healthcheck from oslo_middleware.healthcheck import __main__ class HealthcheckMainTests(test_base.BaseTestCase): def test_startup_response(self): server = __main__.create_server(0) th = threading.Thread(target=server.serve_forever) th.start() self.addCleanup(server.shutdown) while True: try: # Connecting on 0.0.0.0 is not allowed on windows # The operating system will return WSAEADDRNOTAVAIL which # in turn will throw a requests.ConnectionError r = requests.get("http://127.0.0.1:%s" % ( server.server_address[1]), timeout=10) except requests.ConnectionError: # Server hasn't started up yet, try again in a few. time.sleep(1) else: self.assertEqual(200, r.status_code) break class HealthcheckTests(test_base.BaseTestCase): def setUp(self): super(HealthcheckTests, self).setUp() self.useFixture(config.Config()) @staticmethod @webob.dec.wsgify def application(req): return 'Hello, World!!!' def _do_test_request(self, conf={}, path='/healthcheck', accept='text/plain', method='GET', server_port=80, headers=None, remote_addr='127.0.0.1'): self.app = healthcheck.Healthcheck(self.application, conf) req = webob.Request.blank(path, accept=accept, method=method) req.server_port = server_port if headers: req.headers = headers req.remote_addr = remote_addr res = req.get_response(self.app) return res def _do_test(self, conf={}, path='/healthcheck', expected_code=webob.exc.HTTPOk.code, expected_body=b'', accept='text/plain', method='GET', server_port=80, headers=None, remote_addr='127.0.0.1'): res = self._do_test_request(conf=conf, path=path, accept=accept, method=method, server_port=server_port, headers=headers, remote_addr=remote_addr) self.assertEqual(expected_code, res.status_int) self.assertEqual(expected_body, res.body) def test_default_path_match(self): self._do_test() def test_default_path_not_match(self): self._do_test(path='/toto', expected_body=b'Hello, World!!!') def test_configured_path_match(self): conf = {'path': '/hidden_healthcheck'} self._do_test(conf, path='/hidden_healthcheck') def test_configured_path_not_match(self): conf = {'path': '/hidden_healthcheck'} self._do_test(conf, path='/toto', expected_body=b'Hello, World!!!') @mock.patch('oslo_middleware.healthcheck.disable_by_file.LOG') def test_disablefile_unconfigured(self, fake_log): fake_warn = fake_log.warning conf = {'backends': 'disable_by_file'} self._do_test(conf, expected_body=b'OK') self.assertIn('disable_by_file', self.app._backends.names()) fake_warn.assert_called_once_with( 'DisableByFile healthcheck middleware ' 'enabled without disable_by_file_path ' 'set' ) def test_disablefile_enabled(self): conf = {'backends': 'disable_by_file', 'disable_by_file_path': '/foobar'} self._do_test(conf, expected_body=b'OK') self.assertIn('disable_by_file', self.app._backends.names()) def test_disablefile_enabled_head(self): conf = {'backends': 'disable_by_file', 'disable_by_file_path': '/foobar'} self._do_test(conf, expected_body=b'', method='HEAD', expected_code=webob.exc.HTTPNoContent.code) def test_disablefile_enabled_html_detailed(self): conf = {'backends': 'disable_by_file', 'disable_by_file_path': '/foobar', 'detailed': True} res = self._do_test_request(conf, accept="text/html") self.assertIn(b'Result of 1 checks:', res.body) self.assertIn(b'OK', res.body) self.assertEqual(webob.exc.HTTPOk.code, res.status_int) def test_disablefile_disabled(self): filename = self.create_tempfiles([('test', 'foobar')])[0] conf = {'backends': 'disable_by_file', 'disable_by_file_path': filename} self._do_test(conf, expected_code=webob.exc.HTTPServiceUnavailable.code, expected_body=b'DISABLED BY FILE') self.assertIn('disable_by_file', self.app._backends.names()) def test_disablefile_disabled_head(self): filename = self.create_tempfiles([('test', 'foobar')])[0] conf = {'backends': 'disable_by_file', 'disable_by_file_path': filename} self._do_test(conf, expected_code=webob.exc.HTTPServiceUnavailable.code, expected_body=b'', method='HEAD') self.assertIn('disable_by_file', self.app._backends.names()) def test_disablefile_disabled_html_detailed(self): filename = self.create_tempfiles([('test', 'foobar')])[0] conf = {'backends': 'disable_by_file', 'disable_by_file_path': filename, 'detailed': True} res = self._do_test_request(conf, accept="text/html") self.assertIn(b'DISABLED BY FILE', res.body) self.assertEqual(webob.exc.HTTPServiceUnavailable.code, res.status_int) def test_two_backends(self): filename = self.create_tempfiles([('test', 'foobar')])[0] conf = {'backends': 'disable_by_file,disable_by_file', 'disable_by_file_path': filename} self._do_test(conf, expected_code=webob.exc.HTTPServiceUnavailable.code, expected_body=b'DISABLED BY FILE\nDISABLED BY FILE') self.assertIn('disable_by_file', self.app._backends.names()) def test_disable_by_port_file(self): filename = self.create_tempfiles([('test', 'foobar')])[0] conf = {'backends': 'disable_by_files_ports', 'disable_by_file_paths': "80:%s" % filename} self._do_test(conf, expected_code=webob.exc.HTTPServiceUnavailable.code, expected_body=b'DISABLED BY FILE') self.assertIn('disable_by_files_ports', self.app._backends.names()) def test_no_disable_by_port_file(self): filename = self.create_tempfiles([('test', 'foobar')])[0] conf = {'backends': 'disable_by_files_ports', 'disable_by_file_paths': "8000:%s" % filename} self._do_test(conf, expected_code=webob.exc.HTTPOk.code, expected_body=b'OK') self.assertIn('disable_by_files_ports', self.app._backends.names()) def test_disable_by_port_many_files(self): filename = self.create_tempfiles([('test', 'foobar')])[0] filename2 = self.create_tempfiles([('test2', 'foobar2')])[0] conf = {'backends': 'disable_by_files_ports', 'disable_by_file_paths': "80:%s,81:%s" % (filename, filename2)} self._do_test(conf, expected_code=webob.exc.HTTPServiceUnavailable.code, expected_body=b'DISABLED BY FILE') self._do_test(conf, expected_code=webob.exc.HTTPServiceUnavailable.code, expected_body=b'DISABLED BY FILE', server_port=81) self.assertIn('disable_by_files_ports', self.app._backends.names()) def test_json_response(self): expected_body = jsonutils.dumps({'detailed': False, 'reasons': []}, indent=4, sort_keys=True).encode('utf-8') self._do_test(expected_body=expected_body, accept='application/json') def test_source_within_allowed_ranges(self): conf = {'allowed_source_ranges': ['192.168.0.0/24', '192.168.1.0/24']} self._do_test(conf, expected_code=webob.exc.HTTPOk.code, remote_addr='192.168.0.1') def test_source_out_of_allowed_ranges(self): conf = {'allowed_source_ranges': ['192.168.0.0/24', '192.168.1.0/24']} self._do_test(conf, expected_code=webob.exc.HTTPOk.code, expected_body=b'Hello, World!!!', remote_addr='192.168.3.1') def test_proxied_not_ignored(self): conf = {} self._do_test(conf, expected_code=webob.exc.HTTPOk.code, headers={'Forwarded-For': 'http://localhost'}) def test_proxied_ignored(self): conf = {'ignore_proxied_requests': True} modern_headers = { 'x-forwarded': 'https://localhost' } self._do_test(conf, expected_code=webob.exc.HTTPOk.code, expected_body=b'Hello, World!!!', headers=modern_headers) legacy_headers = { 'x-forwarded-proto': 'https', 'x-forwarded-host': 'localhost', 'x-forwarded-for': '192.0.2.11', } self._do_test(conf, expected_code=webob.exc.HTTPOk.code, expected_body=b'Hello, World!!!', headers=legacy_headers) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_http_proxy_to_wsgi.py0000664000175000017500000001561700000000000026542 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from wsgiref import util from oslotest import base as test_base import webob from oslo_middleware import http_proxy_to_wsgi class TestHTTPProxyToWSGI(test_base.BaseTestCase): def setUp(self): super(TestHTTPProxyToWSGI, self).setUp() @webob.dec.wsgify() def fake_app(req): return util.application_uri(req.environ) self.middleware = http_proxy_to_wsgi.HTTPProxyToWSGI(fake_app) self.middleware.oslo_conf.set_override('enable_proxy_headers_parsing', True, group='oslo_middleware') self.request = webob.Request.blank('/foo/bar', method='POST') def test_backward_compat(self): @webob.dec.wsgify() def fake_app(req): return util.application_uri(req.environ) self.middleware = http_proxy_to_wsgi.HTTPProxyToWSGI(fake_app) response = self.request.get_response(self.middleware) self.assertEqual(b"http://localhost:80/", response.body) def test_no_headers(self): response = self.request.get_response(self.middleware) self.assertEqual(b"http://localhost:80/", response.body) def test_url_translate_ssl(self): self.request.headers['X-Forwarded-Proto'] = "https" response = self.request.get_response(self.middleware) self.assertEqual(b"https://localhost:80/", response.body) def test_url_translate_ssl_port(self): self.request.headers['X-Forwarded-Proto'] = "https" self.request.headers['X-Forwarded-Host'] = "example.com:123" response = self.request.get_response(self.middleware) self.assertEqual(b"https://example.com:123/", response.body) def test_url_translate_host_ipv6(self): self.request.headers['X-Forwarded-Proto'] = "https" self.request.headers['X-Forwarded-Host'] = "[f00:b4d::1]:123" response = self.request.get_response(self.middleware) self.assertEqual(b"https://[f00:b4d::1]:123/", response.body) def test_url_translate_base(self): self.request.headers['X-Forwarded-Prefix'] = "/bla" response = self.request.get_response(self.middleware) self.assertEqual(b"http://localhost:80/bla", response.body) def test_url_translate_port_and_base_and_proto_and_host(self): self.request.headers['X-Forwarded-Proto'] = "https" self.request.headers['X-Forwarded-Prefix'] = "/bla" self.request.headers['X-Forwarded-Host'] = "example.com:8043" response = self.request.get_response(self.middleware) self.assertEqual(b"https://example.com:8043/bla", response.body) def test_rfc7239_invalid(self): self.request.headers['Forwarded'] = ( "iam=anattacker;metoo, I will crash you!!P;m,xx") response = self.request.get_response(self.middleware) self.assertEqual(b"http://localhost:80/", response.body) def test_rfc7239_proto(self): self.request.headers['Forwarded'] = ( "for=foobar;proto=https, for=foobaz;proto=http") response = self.request.get_response(self.middleware) self.assertEqual(b"https://localhost:80/", response.body) def test__parse_rfc7239_header(self): expected_result = [{'for': 'foobar', 'proto': 'https'}, {'for': 'foobaz', 'proto': 'http'}] result = self.middleware._parse_rfc7239_header( "for=foobar;proto=https, for=foobaz;proto=http") self.assertEqual(expected_result, result) result = self.middleware._parse_rfc7239_header( "for=foobar; proto=https, for=foobaz; proto=http") self.assertEqual(expected_result, result) def test_rfc7239_proto_host(self): self.request.headers['Forwarded'] = ( "for=foobar;proto=https;host=example.com, for=foobaz;proto=http") response = self.request.get_response(self.middleware) self.assertEqual(b"https://example.com/", response.body) def test_rfc7239_proto_host_base(self): self.request.headers['Forwarded'] = ( "for=foobar;proto=https;host=example.com:8043, for=foobaz") self.request.headers['X-Forwarded-Prefix'] = "/bla" response = self.request.get_response(self.middleware) self.assertEqual(b"https://example.com:8043/bla", response.body) def test_forwarded_for_headers(self): @webob.dec.wsgify() def fake_app(req): return req.environ['REMOTE_ADDR'] self.middleware = http_proxy_to_wsgi.HTTPProxyToWSGI(fake_app) forwarded_for_addr = '1.2.3.4' forwarded_addr = '8.8.8.8' # If both X-Forwarded-For and Fowarded headers are present, it should # use the Forwarded header and ignore the X-Forwarded-For header. self.request.headers['Forwarded'] = ( "for=%s;proto=https;host=example.com:8043" % (forwarded_addr)) self.request.headers['X-Forwarded-For'] = forwarded_for_addr response = self.request.get_response(self.middleware) self.assertEqual(forwarded_addr.encode(), response.body) # Now if only X-Forwarded-For header is present, it should be used. del self.request.headers['Forwarded'] response = self.request.get_response(self.middleware) self.assertEqual(forwarded_for_addr.encode(), response.body) class TestHTTPProxyToWSGIDisabled(test_base.BaseTestCase): def setUp(self): super(TestHTTPProxyToWSGIDisabled, self).setUp() @webob.dec.wsgify() def fake_app(req): return util.application_uri(req.environ) self.middleware = http_proxy_to_wsgi.HTTPProxyToWSGI(fake_app) self.middleware.oslo_conf.set_override('enable_proxy_headers_parsing', False, group='oslo_middleware') self.request = webob.Request.blank('/foo/bar', method='POST') def test_no_headers(self): response = self.request.get_response(self.middleware) self.assertEqual(b"http://localhost:80/", response.body) def test_url_translate_ssl_has_no_effect(self): self.request.headers['X-Forwarded-Proto'] = "https" self.request.headers['X-Forwarded-Host'] = "example.com:123" response = self.request.get_response(self.middleware) self.assertEqual(b"http://localhost:80/", response.body) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_opts.py0000664000175000017500000000157500000000000023552 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_middleware import opts from oslotest.base import BaseTestCase class TestOptionDiscovery(BaseTestCase): def test_all(self): opts.list_opts() def test_sizelimit(self): opts.list_opts_sizelimit() def test_cors(self): opts.list_opts_cors() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_request_id.py0000664000175000017500000000765700000000000024740 0ustar00zuulzuul00000000000000# Copyright (c) 2013 NEC Corporation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from oslotest import base as test_base from testtools import matchers import webob import webob.dec from oslo_middleware import request_id class AltHeader(request_id.RequestId): compat_headers = ["x-compute-req-id", "x-silly-id"] class RequestIdTest(test_base.BaseTestCase): def test_generate_request_id(self): @webob.dec.wsgify def application(req): return req.environ[request_id.ENV_REQUEST_ID] app = request_id.RequestId(application) req = webob.Request.blank('/test') res = req.get_response(app) res_req_id = res.headers.get(request_id.HTTP_RESP_HEADER_REQUEST_ID) if isinstance(res_req_id, bytes): res_req_id = res_req_id.decode('utf-8') self.assertThat(res_req_id, matchers.StartsWith('req-')) # request-id in request environ is returned as response body self.assertEqual(res.body.decode('utf-8'), res_req_id) def test_compat_headers(self): """Test that compat headers are set Compat headers might exist on a super class to support previous API contracts. This ensures that you can set that to a list of headers and those values are the same as the request_id. """ @webob.dec.wsgify def application(req): return req.environ[request_id.ENV_REQUEST_ID] app = AltHeader(application) req = webob.Request.blank('/test') res = req.get_response(app) res_req_id = res.headers.get(request_id.HTTP_RESP_HEADER_REQUEST_ID) self.assertEqual(res.headers.get("x-compute-req-id"), res_req_id) self.assertEqual(res.headers.get("x-silly-id"), res_req_id) def test_global_request_id_set(self): """Test that global request_id is set.""" @webob.dec.wsgify def application(req): return req.environ[request_id.GLOBAL_REQ_ID] global_req = "req-%s" % uuid.uuid4() app = request_id.RequestId(application) req = webob.Request.blank( '/test', headers={"X-OpenStack-Request-ID": global_req}) res = req.get_response(app) res_req_id = res.headers.get(request_id.HTTP_RESP_HEADER_REQUEST_ID) if isinstance(res_req_id, bytes): res_req_id = res_req_id.decode('utf-8') # global-request-id in request environ is returned as response body self.assertEqual(res.body.decode('utf-8'), global_req) self.assertNotEqual(res.body.decode('utf-8'), res_req_id) def test_global_request_id_drop(self): """Test that bad format ids are dropped. This ensures that badly formatted ids are dropped entirely. """ @webob.dec.wsgify def application(req): return req.environ.get(request_id.GLOBAL_REQ_ID) global_req = "req-%s-bad" % uuid.uuid4() app = request_id.RequestId(application) req = webob.Request.blank( '/test', headers={"X-OpenStack-Request-ID": global_req}) res = req.get_response(app) res_req_id = res.headers.get(request_id.HTTP_RESP_HEADER_REQUEST_ID) if isinstance(res_req_id, bytes): res_req_id = res_req_id.decode('utf-8') # global-request-id in request environ is returned as response body self.assertEqual(res.body.decode('utf-8'), '') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_sizelimit.py0000664000175000017500000000672600000000000024601 0ustar00zuulzuul00000000000000# Copyright (c) 2012 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import io from oslo_config import fixture as config from oslotest import base as test_base import webob from oslo_middleware import sizelimit class TestLimitingReader(test_base.BaseTestCase): def test_limiting_reader(self): BYTES = 1024 bytes_read = 0 data = io.StringIO("*" * BYTES) for chunk in sizelimit.LimitingReader(data, BYTES): bytes_read += len(chunk) self.assertEqual(BYTES, bytes_read) bytes_read = 0 data = io.StringIO("*" * BYTES) reader = sizelimit.LimitingReader(data, BYTES) byte = reader.read(1) while len(byte) != 0: bytes_read += 1 byte = reader.read(1) self.assertEqual(BYTES, bytes_read) def test_read_default_value(self): BYTES = 1024 data_str = "*" * BYTES data = io.StringIO(data_str) reader = sizelimit.LimitingReader(data, BYTES) res = reader.read() self.assertEqual(data_str, res) def test_limiting_reader_fails(self): BYTES = 1024 def _consume_all_iter(): bytes_read = 0 data = io.StringIO("*" * BYTES) for chunk in sizelimit.LimitingReader(data, BYTES - 1): bytes_read += len(chunk) self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, _consume_all_iter) def _consume_all_read(): bytes_read = 0 data = io.StringIO("*" * BYTES) reader = sizelimit.LimitingReader(data, BYTES - 1) byte = reader.read(1) while len(byte) != 0: bytes_read += 1 byte = reader.read(1) self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, _consume_all_read) class TestRequestBodySizeLimiter(test_base.BaseTestCase): def setUp(self): super(TestRequestBodySizeLimiter, self).setUp() self.useFixture(config.Config()) @webob.dec.wsgify() def fake_app(req): return webob.Response(req.body) self.middleware = sizelimit.RequestBodySizeLimiter(fake_app) self.MAX_REQUEST_BODY_SIZE = ( self.middleware.oslo_conf.oslo_middleware.max_request_body_size) self.request = webob.Request.blank('/', method='POST') def test_content_length_acceptable(self): self.request.headers['Content-Length'] = self.MAX_REQUEST_BODY_SIZE self.request.body = b"0" * self.MAX_REQUEST_BODY_SIZE response = self.request.get_response(self.middleware) self.assertEqual(200, response.status_int) def test_content_length_too_large(self): self.request.headers['Content-Length'] = self.MAX_REQUEST_BODY_SIZE + 1 self.request.body = b"0" * (self.MAX_REQUEST_BODY_SIZE + 1) response = self.request.get_response(self.middleware) self.assertEqual(413, response.status_int) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/tests/test_stats.py0000664000175000017500000001151600000000000023717 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Cisco Systems # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import uuid from oslotest import base as test_base import statsd import webob.dec import webob.exc from oslo_middleware import stats class TestStaticMethods(test_base.BaseTestCase): def test_removes_uuid(self): # Generate a long-format UUID (standard form). id = str(uuid.uuid4()) path = "foo.{uuid}.bar".format(uuid=id) stat = stats.StatsMiddleware.strip_uuid(path) self.assertEqual("foo.bar", stat) def test_removes_short_uuid(self): id = uuid.uuid4().hex path = "foo.{uuid}.bar".format(uuid=id) stat = stats.StatsMiddleware.strip_short_uuid(path) self.assertEqual("foo.bar", stat) def test_strips_dots_from_version(self): path = "/v1.2/foo.bar/bar.foo" stat = stats.StatsMiddleware.strip_dot_from_version(path) self.assertEqual("/v12/foo.bar/bar.foo", stat) class TestStatsMiddleware(test_base.BaseTestCase): def setUp(self): super(TestStatsMiddleware, self).setUp() self.patch(statsd, 'StatsClient', mock.MagicMock()) def make_stats_middleware(self, stat_name=None, stats_host=None, remove_uuid=False, remove_short_uuid=False): if stat_name is None: stat_name = uuid.uuid4().hex if stats_host is None: stats_host = uuid.uuid4().hex conf = dict( name=stat_name, stats_host=stats_host, remove_uuid=remove_uuid, remove_short_uuid=remove_short_uuid, ) @webob.dec.wsgify def fake_application(req): return 'Hello, World' return stats.StatsMiddleware(fake_application, conf) def perform_request(self, app, path, method): req = webob.Request.blank(path, method=method) return req.get_response(app) def test_sends_counter_to_statsd(self): app = self.make_stats_middleware() path = '/test/foo/bar' self.perform_request(app, path, 'GET') expected_stat = "{name}.{method}.{path}".format( name=app.stat_name, method='GET', path=path.lstrip('/').replace('/', '.')) app.statsd.timer.assert_called_once_with(expected_stat) def test_strips_uuid_if_configured(self): app = self.make_stats_middleware(remove_uuid=True) random_uuid = str(uuid.uuid4()) path = '/foo/{uuid}/bar'.format(uuid=random_uuid) self.perform_request(app, path, 'GET') expected_stat = "{name}.{method}.foo.bar".format( name=app.stat_name, method='GET') app.statsd.timer.assert_called_once_with(expected_stat) def test_strips_short_uuid_if_configured(self): app = self.make_stats_middleware(remove_short_uuid=True) random_uuid = uuid.uuid4().hex path = '/foo/{uuid}/bar'.format(uuid=random_uuid) self.perform_request(app, path, 'GET') expected_stat = "{name}.{method}.foo.bar".format( name=app.stat_name, method='GET') app.statsd.timer.assert_called_once_with(expected_stat) def test_strips_both_uuid_types_if_configured(self): app = self.make_stats_middleware( remove_uuid=True, remove_short_uuid=True) random_short_uuid = uuid.uuid4().hex random_uuid = str(uuid.uuid4()) path = '/foo/{uuid}/bar/{short_uuid}'.format( uuid=random_uuid, short_uuid=random_short_uuid) self.perform_request(app, path, 'GET') expected_stat = "{name}.{method}.foo.bar".format( name=app.stat_name, method='GET') app.statsd.timer.assert_called_once_with(expected_stat) def test_always_mutates_version_id(self): app = self.make_stats_middleware() path = '/v2.1/foo/bar' self.perform_request(app, path, 'GET') expected_stat = "{name}.{method}.v21.foo.bar".format( name=app.stat_name, method='GET') app.statsd.timer.assert_called_once_with(expected_stat) def test_empty_path_has_sane_stat_name(self): app = self.make_stats_middleware() path = '/' self.perform_request(app, path, 'GET') expected_stat = "{name}.{method}".format( name=app.stat_name, method='GET') app.statsd.timer.assert_called_once_with(expected_stat) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/oslo_middleware/version.py0000664000175000017500000000126700000000000022047 0ustar00zuulzuul00000000000000# Copyright 2016 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version version_info = pbr.version.VersionInfo('oslo.middleware') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708612004.429549 oslo.middleware-6.1.0/releasenotes/0000775000175000017500000000000000000000000017322 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4455523 oslo.middleware-6.1.0/releasenotes/notes/0000775000175000017500000000000000000000000020452 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/add_reno-3b4ae0789e9c45b4.yaml0000664000175000017500000000007100000000000025333 0ustar00zuulzuul00000000000000--- other: - Switch to reno for managing release notes.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/basic-auth-middleware-5f812399e325425f.yaml0000664000175000017500000000114100000000000027476 0ustar00zuulzuul00000000000000--- features: - | Adds a basic http auth middleware as an alternative to noauth in standalone environments. This middleware uses a password file which supports the Apache `htpasswd`_ syntax. This file is read for every request, so no service restart is required when changes are made. The only password digest supported is bcrypt, and the ``bcrypt`` python library is used for password checks since it supports ``$2y$`` prefixed bcrypt passwords as generated by the Apache htpasswd utility. .. _htpasswd: https://httpd.apache.org/docs/current/misc/password_encryptions.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/compat_headers-55a635b8ec01b6f1.yaml0000664000175000017500000000044100000000000026523 0ustar00zuulzuul00000000000000--- features: - | This adds a new ``compat_headers`` class attribute to the ``RequestId`` middleware. That allows projects like Nova that have API contracts on alternative request-id headers to adopt the oslo ``RequestId`` middleware but still retain their API contract. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/drop-python27-support-a6361831195bf29c.yaml0000664000175000017500000000017700000000000027570 0ustar00zuulzuul00000000000000--- upgrade: - | Support for Python 2.7 has been dropped. The minimum version of Python now supported is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/global_request_id-a8ec7260fbd76444.yaml0000664000175000017500000000047200000000000027247 0ustar00zuulzuul00000000000000--- features: - | This adds support for ``global_request_id`` to the ``RequestId`` middleware. An inbound header of ``X-OpenStack-Request-ID`` is accepted as long as it is of the format ``req-$uuid``, and made available to oslo.context. This will allow for cross project request id tracking. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/healthcheck-allowed_source_ranges-9cbaf89f65914851.yaml0000664000175000017500000000032500000000000032325 0ustar00zuulzuul00000000000000--- features: - | The new ``[healthcheck] allowed_source_ranges`` parameter has been added. This parameter defines a list of network ranges from which access to ``/healthcheck`` endpoint is allowed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/healthcheck-ignore_proxied_requests-d04d1661bd687bc6.yaml0000664000175000017500000000066500000000000032773 0ustar00zuulzuul00000000000000--- features: - | The new ``[healthcheck] ignore_proxied_requests`` option has been added. When this option is set to true, the healthcheck middleware ignores requests with any of the following headers, which indicates that the requests came through a reverse proxy or a load balancer. - ``x-forwarded`` - ``x-forwarded-proto`` - ``x-forwarded-host`` - ``x-forwarded-for`` - ``x-forwarded-prefix`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/log_max_request_body_size-1835363-6f37946210a100d7.yaml0000664000175000017500000000010100000000000031413 0ustar00zuulzuul00000000000000--- other: - | Log when max_request_body_size is exceeded. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/remove-oslo-namespace-b0f050f074d12aff.yaml0000664000175000017500000000024400000000000030016 0ustar00zuulzuul00000000000000--- upgrade: - | The ``olso.middleware`` package has been removed. This package was earlier deprecated to be replaced by the ``oslo_middleware`` package. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/notes/remove-ssl-middleware-0ce922b05f83c447.yaml0000664000175000017500000000020300000000000027674 0ustar00zuulzuul00000000000000--- deprecations: - | The SSL middleware has been removed. It was deprecated in favor of the HTTPProxyToWSGI middleware. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/releasenotes/source/0000775000175000017500000000000000000000000020622 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/2023.1.rst0000664000175000017500000000020200000000000022073 0ustar00zuulzuul00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/2023.2.rst0000664000175000017500000000020200000000000022074 0ustar00zuulzuul00000000000000=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/releasenotes/source/_static/0000775000175000017500000000000000000000000022250 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000024521 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000022757 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000025230 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/conf.py0000664000175000017500000001646700000000000022137 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # 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. # 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.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'reno.sphinxext', 'openstackdocstheme' ] # openstackdocstheme options openstackdocs_repo_name = 'openstack/oslo.middleware' openstackdocs_bug_project = 'oslo.middleware' openstackdocs_bug_tag = '' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. copyright = '2016, oslo.middleware Developers' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # 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 = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'oslo.middlewareReleaseNotes', 'oslo.middleware Release Notes Documentation', ['oslo.middleware Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'oslo.middlewareReleaseNotes', 'oslo.middleware Release Notes Documentation', 'oslo.middleware Developers', 'oslo.middlewareReleaseNotes', 'The library includes components that can be injected into wsgi pipelines' ' to intercept request/response flows.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/index.rst0000664000175000017500000000041100000000000022457 0ustar00zuulzuul00000000000000============================= oslo.middleware Release Notes ============================= .. toctree:: :maxdepth: 1 unreleased 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708612004.429549 oslo.middleware-6.1.0/releasenotes/source/locale/0000775000175000017500000000000000000000000022061 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708612004.429549 oslo.middleware-6.1.0/releasenotes/source/locale/en_GB/0000775000175000017500000000000000000000000023033 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000024620 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000001105700000000000027655 0ustar00zuulzuul00000000000000# Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata # Andi Chandler , 2023. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.middleware\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-09-07 13:30+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2023-09-21 01:02+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "2023.1 Series Release Notes" msgstr "2023.1 Series Release Notes" msgid "2023.2 Series Release Notes" msgstr "2023.2 Series Release Notes" msgid "3.20.0" msgstr "3.20.0" msgid "3.27.0" msgstr "3.27.0" msgid "3.38.1" msgstr "3.38.1" msgid "4.0.0" msgstr "4.0.0" msgid "4.4.0" msgstr "4.4.0" msgid "" "Adds a basic http auth middleware as an alternative to noauth in standalone " "environments. This middleware uses a password file which supports the Apache " "`htpasswd`_ syntax. This file is read for every request, so no service " "restart is required when changes are made. The only password digest " "supported is bcrypt, and the ``bcrypt`` python library is used for password " "checks since it supports ``$2y$`` prefixed bcrypt passwords as generated by " "the Apache htpasswd utility." msgstr "" "Adds a basic HTTP auth middleware as an alternative to noauth in standalone " "environments. This middleware uses a password file which supports the Apache " "`htpasswd`_ syntax. This file is read for every request, so no service " "restart is required when changes are made. The only password digest " "supported is bcrypt, and the ``bcrypt`` python library is used for password " "checks since it supports ``$2y$`` prefixed bcrypt passwords as generated by " "the Apache htpasswd utility." msgid "Log when max_request_body_size is exceeded." msgstr "Log when max_request_body_size is exceeded." msgid "New Features" msgstr "New Features" msgid "Ocata Series Release Notes" msgstr "Ocata Series Release Notes" msgid "Other Notes" msgstr "Other Notes" msgid "Pike Series Release Notes" msgstr "Pike Series Release Notes" msgid "Queens Series Release Notes" msgstr "Queens Series Release Notes" msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Stein Series Release Notes" msgstr "Stein Series Release Notes" msgid "" "Support for Python 2.7 has been dropped. The minimum version of Python now " "supported is Python 3.6." msgstr "" "Support for Python 2.7 has been dropped. The minimum version of Python now " "supported is Python 3.6." msgid "Switch to reno for managing release notes." msgstr "Switch to Reno for managing release notes." msgid "" "This adds a new ``compat_headers`` class attribute to the ``RequestId`` " "middleware. That allows projects like Nova that have API contracts on " "alternative request-id headers to adopt the oslo ``RequestId`` middleware " "but still retain their API contract." msgstr "" "This adds a new ``compat_headers`` class attribute to the ``RequestId`` " "middleware. That allows projects like Nova that have API contracts on " "alternative request-id headers to adopt the oslo ``RequestId`` middleware " "but still retain their API contract." msgid "" "This adds support for ``global_request_id`` to the ``RequestId`` middleware. " "An inbound header of ``X-OpenStack-Request-ID`` is accepted as long as it is " "of the format ``req-$uuid``, and made available to oslo.context. This will " "allow for cross project request id tracking." msgstr "" "This adds support for ``global_request_id`` to the ``RequestId`` middleware. " "An inbound header of ``X-OpenStack-Request-ID`` is accepted as long as it is " "of the format ``req-$uuid``, and made available to oslo.context. This will " "allow for cross project request id tracking." msgid "Train Series Release Notes" msgstr "Train Series Release Notes" msgid "Unreleased Release Notes" msgstr "Unreleased Release Notes" msgid "Upgrade Notes" msgstr "Upgrade Notes" msgid "Ussuri Series Release Notes" msgstr "Ussuri Series Release Notes" msgid "Victoria Series Release Notes" msgstr "Victoria Series Release Notes" msgid "Wallaby Series Release Notes" msgstr "Wallaby Series Release Notes" msgid "Xena Series Release Notes" msgstr "Xena Series Release Notes" msgid "Yoga Series Release Notes" msgstr "Yoga Series Release Notes" msgid "Zed Series Release Notes" msgstr "Zed Series Release Notes" msgid "oslo.middleware Release Notes" msgstr "oslo.middleware Release Notes" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708612004.429549 oslo.middleware-6.1.0/releasenotes/source/locale/fr/0000775000175000017500000000000000000000000022470 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/releasenotes/source/locale/fr/LC_MESSAGES/0000775000175000017500000000000000000000000024255 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po0000664000175000017500000000155200000000000027311 0ustar00zuulzuul00000000000000# Gérald LONLAS , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.middleware Release Notes 3.20.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-11-02 02:56+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-10-22 06:02+0000\n" "Last-Translator: Gérald LONLAS \n" "Language-Team: French\n" "Language: fr\n" "X-Generator: Zanata 3.7.3\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" msgid "Other Notes" msgstr "Autres notes" msgid "Switch to reno for managing release notes." msgstr "Commence à utiliser reno pour la gestion des notes de release" msgid "Unreleased Release Notes" msgstr "Note de release pour les changements non déployées" msgid "oslo.middleware Release Notes" msgstr "Note de release pour oslo.middleware" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000022436 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000022304 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000022651 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000022476 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000022471 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/train.rst0000664000175000017500000000017600000000000022475 0ustar00zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/unreleased.rst0000664000175000017500000000014400000000000023502 0ustar00zuulzuul00000000000000========================== Unreleased Release Notes ========================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000022700 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000023167 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/wallaby.rst0000664000175000017500000000020600000000000023005 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/xena.rst0000664000175000017500000000017200000000000022307 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/yoga.rst0000664000175000017500000000020000000000000022303 0ustar00zuulzuul00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/releasenotes/source/zed.rst0000664000175000017500000000016600000000000022141 0ustar00zuulzuul00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/requirements.txt0000664000175000017500000000051200000000000020113 0ustar00zuulzuul00000000000000pbr!=2.1.0,>=2.0.0 # Apache-2.0 Jinja2>=2.10 # BSD License (3 clause) oslo.config>=5.2.0 # Apache-2.0 oslo.context>=2.19.2 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0 WebOb>=1.8.0 # MIT debtcollector>=1.2.0 # Apache-2.0 statsd>=3.2.1 # MIT bcrypt>=3.1.3 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708612004.4495533 oslo.middleware-6.1.0/setup.cfg0000664000175000017500000000410700000000000016454 0ustar00zuulzuul00000000000000[metadata] name = oslo.middleware summary = Oslo Middleware library description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/oslo.middleware/latest/ python_requires = >=3.8 classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython [files] packages = oslo_middleware [entry_points] oslo.config.opts = oslo.middleware = oslo_middleware.opts:list_opts oslo.middleware.cors = oslo_middleware.opts:list_opts_cors oslo.middleware.sizelimit = oslo_middleware.opts:list_opts_sizelimit oslo.middleware.http_proxy_to_wsgi = oslo_middleware.opts:list_opts_http_proxy_to_wsgi oslo.middleware.healthcheck = oslo_middleware.opts:list_opts_healthcheck oslo.middleware.basic_auth = oslo_middleware.opts:list_opts_basic_auth oslo.middleware.healthcheck = disable_by_file = oslo_middleware.healthcheck.disable_by_file:DisableByFileHealthcheck disable_by_files_ports = oslo_middleware.healthcheck.disable_by_file:DisableByFilesPortsHealthcheck paste.app_factory = healthcheck = oslo_middleware:Healthcheck.app_factory paste.filter_factory = basic_auth = oslo_middleware:BasicAuthMiddleware.factory catch_errors = oslo_middleware:CatchErrors.factory correlation_id = oslo_middleware:CorrelationId.factory cors = oslo_middleware:CORS.factory debug = oslo_middleware:Debug.factory healthcheck = oslo_middleware:Healthcheck.factory http_proxy_to_wsgi = oslo_middleware:HTTPProxyToWSGI.factory request_id = oslo_middleware:RequestId.factory sizelimit = oslo_middleware:RequestBodySizeLimiter.factory [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/setup.py0000664000175000017500000000127100000000000016344 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/test-requirements.txt0000664000175000017500000000047700000000000021102 0ustar00zuulzuul00000000000000fixtures>=3.0.0 # Apache-2.0/BSD hacking>=6.1.0,<6.2.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 testtools>=2.2.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 # Bandit security code scanner bandit>=1.7.0,<1.8.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 pre-commit>=2.6.0 # MIT ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708611977.0 oslo.middleware-6.1.0/tox.ini0000664000175000017500000000243500000000000016150 0ustar00zuulzuul00000000000000[tox] minversion = 3.1 envlist = py3,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt commands = stestr run --slowest {posargs} [testenv:pep8] commands = pre-commit run -a # Run security linter bandit -r oslo_middleware -x tests -n5 [testenv:venv] commands = {posargs} [testenv:docs] allowlist_externals = rm deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = rm -fr doc/build sphinx-build -W --keep-going -b html doc/source doc/build/html [testenv:cover] commands = python setup.py test --coverage --coverage-package-name=oslo_middleware --testr-args='{posargs}' [flake8] # E123, E125 skipped as they are invalid PEP-8. # W504 line break after binary operator show-source = True ignore = E123,E125,W504 exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,__init__.py [hacking] import_exceptions = oslo_middleware._i18n [testenv:releasenotes] allowlist_externals = rm deps = {[testenv:docs]deps} commands = rm -rf releasenotes/build sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html