Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:steven.hardy:testing:Cloud:OpenStack:Bobcat
python-oslo.middleware
_service:obs_scm:oslo.middleware-2023.2.0.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File _service:obs_scm:oslo.middleware-2023.2.0.obscpio of Package python-oslo.middleware
07070100000000000081A400000000000000000000000164F999DB00000079000000000000000000000000000000000000002500000000oslo.middleware-2023.2.0/.coveragerc[run] branch = True source = oslo_middleware omit = oslo_middleware/tests/* [report] ignore_errors = True precision = 2 07070100000001000081A400000000000000000000000164F999DB000003C5000000000000000000000000000000000000002400000000oslo.middleware-2023.2.0/.gitignore# Add patterns in here to exclude files created by tools integrated with this # repository, such as test frameworks from the project's recommended workflow, # rendered documentation and package builds. # # Don't add patterns to exclude files created by preferred personal tools # (editors, IDEs, your operating system itself even). These should instead be # maintained outside the repository, for example in a ~/.gitignore file added # with: # # git config --global core.excludesfile '~/.gitignore' # Bytecompiled Python *.py[cod] # C extensions *.so # Packages *.egg *.eggs *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage cover .tox .stestr/ # Translations *.mo # Complexity output/*.html output/*/index.html # Sphinx doc/build # pbr generates these AUTHORS ChangeLog # reno build releasenotes/build RELEASENOTES.rst releasenotes/notes/reno.cache 07070100000002000081A400000000000000000000000164F999DB0000006E000000000000000000000000000000000000002400000000oslo.middleware-2023.2.0/.gitreview[gerrit] host=review.opendev.org port=29418 project=openstack/oslo.middleware.git defaultbranch=stable/2023.2 07070100000003000081A400000000000000000000000164F999DB00000058000000000000000000000000000000000000002200000000oslo.middleware-2023.2.0/.mailmap# Format is: # <preferred e-mail> <other e-mail 1> # <preferred e-mail> <other e-mail 2>07070100000004000081A400000000000000000000000164F999DB00000553000000000000000000000000000000000000003100000000oslo.middleware-2023.2.0/.pre-commit-config.yaml# 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>=3.0.1,<3.1.0 language: python entry: flake8 files: '^.*\.py$' exclude: '^(doc|releasenotes|tools)/.*$' 07070100000005000081A400000000000000000000000164F999DB00000038000000000000000000000000000000000000002600000000oslo.middleware-2023.2.0/.stestr.conf[DEFAULT] test_path=./oslo_middleware/tests top_path=./ 07070100000006000081A400000000000000000000000164F999DB000000DB000000000000000000000000000000000000002400000000oslo.middleware-2023.2.0/.zuul.yaml- project: templates: - check-requirements - lib-forward-testing-python3 - openstack-python3-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 07070100000007000081A400000000000000000000000164F999DB000002EA000000000000000000000000000000000000002A00000000oslo.middleware-2023.2.0/CONTRIBUTING.rstIf 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 07070100000008000081A400000000000000000000000164F999DB00000098000000000000000000000000000000000000002500000000oslo.middleware-2023.2.0/HACKING.rstoslo.middleware Style Commandments ================================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ 07070100000009000081A400000000000000000000000164F999DB0000279E000000000000000000000000000000000000002100000000oslo.middleware-2023.2.0/LICENSE 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. 0707010000000A000081A400000000000000000000000164F999DB0000036A000000000000000000000000000000000000002400000000oslo.middleware-2023.2.0/README.rst=============== 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/ 0707010000000B000041ED00000000000000000000000364F999DB00000000000000000000000000000000000000000000001D00000000oslo.middleware-2023.2.0/doc0707010000000C000081A400000000000000000000000164F999DB00000177000000000000000000000000000000000000002E00000000oslo.middleware-2023.2.0/doc/requirements.txt# 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 0707010000000D000041ED00000000000000000000000764F999DB00000000000000000000000000000000000000000000002400000000oslo.middleware-2023.2.0/doc/source0707010000000E000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000002A00000000oslo.middleware-2023.2.0/doc/source/admin0707010000000F000081A400000000000000000000000164F999DB00001B44000000000000000000000000000000000000004100000000oslo.middleware-2023.2.0/doc/source/admin/cross-project-cors.rst.. _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 <https://docs.openstack.org/ocata/config-reference/common-configurations/cors.html>`_ 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. 07070100000010000081A400000000000000000000000164F999DB0000017F000000000000000000000000000000000000003400000000oslo.middleware-2023.2.0/doc/source/admin/index.rst====================== 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 07070100000011000081A400000000000000000000000164F999DB0000086F000000000000000000000000000000000000002C00000000oslo.middleware-2023.2.0/doc/source/conf.py# -*- 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'] 07070100000012000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000003200000000oslo.middleware-2023.2.0/doc/source/configuration07070100000013000081A400000000000000000000000164F999DB0000065F000000000000000000000000000000000000003C00000000oslo.middleware-2023.2.0/doc/source/configuration/index.rst============================= 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` 07070100000014000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000003000000000oslo.middleware-2023.2.0/doc/source/contributor07070100000015000081A400000000000000000000000164F999DB00000054000000000000000000000000000000000000003A00000000oslo.middleware-2023.2.0/doc/source/contributor/index.rst============== Contributing ============== .. include:: ../../../CONTRIBUTING.rst 07070100000016000081A400000000000000000000000164F999DB000001E2000000000000000000000000000000000000003100000000oslo.middleware-2023.2.0/doc/source/glossary.rst======== 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. 07070100000017000081A400000000000000000000000164F999DB00000144000000000000000000000000000000000000002E00000000oslo.middleware-2023.2.0/doc/source/index.rst.. 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 <https://docs.openstack.org/releasenotes/oslo.middleware/>`_. 07070100000018000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000002C00000000oslo.middleware-2023.2.0/doc/source/install07070100000019000081A400000000000000000000000164F999DB000000D5000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/doc/source/install/index.rst============ Installation ============ At the command line:: $ pip install oslo.middleware Or, if you have virtualenvwrapper installed:: $ mkvirtualenv oslo.middleware $ pip install oslo.middleware0707010000001A000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000002E00000000oslo.middleware-2023.2.0/doc/source/reference0707010000001B000081A400000000000000000000000164F999DB0000010C000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/doc/source/reference/api.rst===== API ===== .. automodule:: oslo_middleware :members: Configuration Options ===================== RequestBodySizeLimiter ~~~~~~~~~~~~~~~~~~~~~~ .. show-options:: oslo.middleware.sizelimit SSLMiddleware ~~~~~~~~~~~~~ .. show-options:: oslo.middleware.ssl 0707010000001C000081A400000000000000000000000164F999DB00000B11000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/doc/source/reference/cors.rst=============== 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/ 0707010000001D000081A400000000000000000000000164F999DB0000014B000000000000000000000000000000000000004600000000oslo.middleware-2023.2.0/doc/source/reference/healthcheck_plugins.rst================================ Healthcheck middleware plugins ================================ .. automodule:: oslo_middleware.healthcheck :members: .. automodule:: oslo_middleware.healthcheck.disable_by_file :members: Available Plugins ------------------ .. list-plugins:: oslo.middleware.healthcheck :detailed: 0707010000001E000081A400000000000000000000000164F999DB0000006C000000000000000000000000000000000000003800000000oslo.middleware-2023.2.0/doc/source/reference/index.rst========================= oslo.middleware Reference ========================= .. toctree:: :glob: * 0707010000001F000041ED00000000000000000000000364F999DB00000000000000000000000000000000000000000000001E00000000oslo.middleware-2023.2.0/oslo07070100000020000081A400000000000000000000000164F999DB00000275000000000000000000000000000000000000002A00000000oslo.middleware-2023.2.0/oslo/__init__.py# 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__('pkg_resources').declare_namespace(__name__) 07070100000021000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000002900000000oslo.middleware-2023.2.0/oslo/middleware07070100000022000081A400000000000000000000000164F999DB00000796000000000000000000000000000000000000003500000000oslo.middleware-2023.2.0/oslo/middleware/__init__.py# 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 sys import warnings def deprecated(): new_name = __name__.replace('.', '_') warnings.warn( ('The oslo namespace package is deprecated. Please use %s instead.' % new_name), DeprecationWarning, stacklevel=3, ) # NOTE(dims): We cannot remove the deprecation or redirects below # until Liberty-EOL deprecated() from oslo_middleware import base from oslo_middleware import catch_errors from oslo_middleware import correlation_id from oslo_middleware import debug from oslo_middleware import request_id from oslo_middleware import sizelimit sys.modules['oslo.middleware.base'] = base sys.modules['oslo.middleware.catch_errors'] = catch_errors sys.modules['oslo.middleware.correlation_id'] = correlation_id sys.modules['oslo.middleware.debug'] = debug sys.modules['oslo.middleware.request_id'] = request_id sys.modules['oslo.middleware.sizelimit'] = sizelimit 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 from oslo_middleware.ssl import SSLMiddleware07070100000023000041ED00000000000000000000000564F999DB00000000000000000000000000000000000000000000002900000000oslo.middleware-2023.2.0/oslo_middleware07070100000024000081A400000000000000000000000164F999DB00000554000000000000000000000000000000000000003500000000oslo.middleware-2023.2.0/oslo_middleware/__init__.py# 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', 'SSLMiddleware'] 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 from oslo_middleware.ssl import SSLMiddleware 07070100000025000081A400000000000000000000000164F999DB00000358000000000000000000000000000000000000003200000000oslo.middleware-2023.2.0/oslo_middleware/_i18n.py# 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 07070100000026000081A400000000000000000000000164F999DB000012F8000000000000000000000000000000000000003100000000oslo.middleware-2023.2.0/oslo_middleware/base.py# 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 07070100000027000081A400000000000000000000000164F999DB00001C11000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/oslo_middleware/basic_auth.py# 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 07070100000028000081A400000000000000000000000164F999DB000005D1000000000000000000000000000000000000003900000000oslo.middleware-2023.2.0/oslo_middleware/catch_errors.py# 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 07070100000029000081A400000000000000000000000164F999DB00000408000000000000000000000000000000000000003B00000000oslo.middleware-2023.2.0/oslo_middleware/correlation_id.py# 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 0707010000002A000081A400000000000000000000000164F999DB00004091000000000000000000000000000000000000003100000000oslo.middleware-2023.2.0/oslo_middleware/cors.py# 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: "<protocol>://<host>[:<port>]", 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 0707010000002B000081A400000000000000000000000164F999DB000006B6000000000000000000000000000000000000003200000000oslo.middleware-2023.2.0/oslo_middleware/debug.py# 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() 0707010000002C000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000003500000000oslo.middleware-2023.2.0/oslo_middleware/healthcheck0707010000002D000081A400000000000000000000000164F999DB0000430D000000000000000000000000000000000000004100000000oslo.middleware-2023.2.0/oslo_middleware/healthcheck/__init__.py# 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 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 <HTML> <HEAD><TITLE>Healthcheck Status</TITLE></HEAD> <BODY> <H2>Result of 1 checks:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> <TR> <TH> Reason </TH> </TR> <TR> <TD>OK</TD> </TR> </TBODY> </TABLE> <HR></HR> </BODY> 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 <HTML> <HEAD><TITLE>Healthcheck Status</TITLE></HEAD> <BODY> <H1>Server status</H1> <B>Server hostname:</B><PRE>...</PRE> <B>Current time:</B><PRE>2016-08-24 06:36:07.302559</PRE> <B>Python version:</B><PRE>2.7.6 (default, Jun 22 2015, 17:58:13) [GCC 4.8.2]</PRE> <B>Platform:</B><PRE>Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty</PRE> <HR></HR> <H2>Garbage collector:</H2> <B>Counts:</B><PRE>(77, 1, 6)</PRE> <B>Thresholds:</B><PRE>(700, 10, 10)</PRE> <HR></HR> <H2>Result of 1 checks:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> <TR> <TH> Kind </TH> <TH> Reason </TH> <TH> Details </TH> </TR> <TR> <TD>HealthcheckResult</TD> <TD>OK</TD> <TD>Path '/tmp/dead' was not found</TD> </TR> </TBODY> </TABLE> <HR></HR> <H2>1 greenthread(s) active:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> <TR> <TD><PRE> File "oslo_middleware/healthcheck/__main__.py", line 94, in <module> main() File "oslo_middleware/healthcheck/__main__.py", line 90, in main server.serve_forever() ... </PRE></TD> </TR> </TBODY> </TABLE> <HR></HR> <H2>1 thread(s) active:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> <TR> <TD><PRE> File "oslo_middleware/healthcheck/__main__.py", line 94, in <module> main() File "oslo_middleware/healthcheck/__main__.py", line 90, in main server.serve_forever() .... </TR> </TBODY> </TABLE> </BODY> </HTML> 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 = """ <HTML> <HEAD><TITLE>Healthcheck Status</TITLE></HEAD> <BODY> {% if detailed -%} <H1>Server status</H1> {% if hostname -%} <B>Server hostname:</B><PRE>{{hostname|e}}</PRE> {%- endif %} <B>Current time:</B><PRE>{{now|e}}</PRE> <B>Python version:</B><PRE>{{python_version|e}}</PRE> <B>Platform:</B><PRE>{{platform|e}}</PRE> <HR></HR> <H2>Garbage collector:</H2> <B>Counts:</B><PRE>{{gc.counts|e}}</PRE> <B>Thresholds:</B><PRE>{{gc.threshold|e}}</PRE> <HR></HR> {%- endif %} <H2>Result of {{results|length}} checks:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> <TR> {% if detailed -%} <TH> Kind </TH> <TH> Reason </TH> <TH> Details </TH> {% else %} <TH> Reason </TH> {%- endif %} </TR> {% for result in results -%} {% if result.reason -%} <TR> {% if detailed -%} <TD>{{result.class|e}}</TD> {%- endif %} <TD>{{result.reason|e}}</TD> {% if detailed -%} <TD>{{result.details|e}}</TD> {%- endif %} </TR> {%- endif %} {%- endfor %} </TBODY> </TABLE> <HR></HR> {% if detailed -%} {% if greenthreads -%} <H2>{{greenthreads|length}} greenthread(s) active:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> {% for stack in greenthreads -%} <TR> <TD><PRE>{{stack|e}}</PRE></TD> </TR> {%- endfor %} </TBODY> </TABLE> <HR></HR> {%- endif %} {% if threads -%} <H2>{{threads|length}} thread(s) active:</H2> <TABLE bgcolor="#ffffff" border="1"> <TBODY> {% for stack in threads -%} <TR> <TD><PRE>{{stack|e}}</PRE></TD> </TR> {%- endfor %} </TBODY> </TABLE> {%- endif %} {%- endif %} </BODY> </HTML> """ 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._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 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) 0707010000002E000081A400000000000000000000000164F999DB000008D1000000000000000000000000000000000000004100000000oslo.middleware-2023.2.0/oslo_middleware/healthcheck/__main__.py# # 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() 0707010000002F000081A400000000000000000000000164F999DB000012B8000000000000000000000000000000000000004800000000oslo.middleware-2023.2.0/oslo_middleware/healthcheck/disable_by_file.py# 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) 07070100000030000081A400000000000000000000000164F999DB0000080C000000000000000000000000000000000000003D00000000oslo.middleware-2023.2.0/oslo_middleware/healthcheck/opts.py# 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.'), ] 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.'), ] 07070100000031000081A400000000000000000000000164F999DB000005D3000000000000000000000000000000000000004300000000oslo.middleware-2023.2.0/oslo_middleware/healthcheck/pluginbase.py# 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) 07070100000032000081A400000000000000000000000164F999DB00000CEC000000000000000000000000000000000000003F00000000oslo.middleware-2023.2.0/oslo_middleware/http_proxy_to_wsgi.py# -*- 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'] 07070100000033000041ED00000000000000000000000364F999DB00000000000000000000000000000000000000000000003000000000oslo.middleware-2023.2.0/oslo_middleware/locale07070100000034000041ED00000000000000000000000364F999DB00000000000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/oslo_middleware/locale/en_GB07070100000035000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000004200000000oslo.middleware-2023.2.0/oslo_middleware/locale/en_GB/LC_MESSAGES07070100000036000081A400000000000000000000000164F999DB000004B0000000000000000000000000000000000000005500000000oslo.middleware-2023.2.0/oslo_middleware/locale/en_GB/LC_MESSAGES/oslo_middleware.po# 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 <andi@gowling.com>, 2014 # Andi Chandler <andi@gowling.com>, 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 <andi@gowling.com>\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)." 07070100000037000081A400000000000000000000000164F999DB00001DC5000000000000000000000000000000000000003100000000oslo.middleware-2023.2.0/oslo_middleware/opts.py# 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 from oslo_middleware import ssl __all__ = [ 'list_opts', 'list_opts_sizelimit', 'list_opts_ssl', '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_ssl(), 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_ssl(): """Return a list of oslo.config options for the SSL 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(ssl.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)), ] 07070100000038000081A400000000000000000000000164F999DB00000917000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/oslo_middleware/request_id.py# 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 07070100000039000081A400000000000000000000000164F999DB00000D87000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/oslo_middleware/sizelimit.py# 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 0707010000003A000081A400000000000000000000000164F999DB00000729000000000000000000000000000000000000003000000000oslo.middleware-2023.2.0/oslo_middleware/ssl.py# 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 debtcollector import removals from oslo_config import cfg from oslo_middleware import base OPTS = [ cfg.StrOpt('secure_proxy_ssl_header', default='X-Forwarded-Proto', deprecated_for_removal=True, help="The HTTP Header that will be used to determine what " "the original request protocol scheme was, even if it was " "hidden by a SSL termination proxy.") ] class SSLMiddleware(base.ConfigurableMiddleware): """SSL termination proxies middleware. This middleware overloads wsgi.url_scheme with the one provided in secure_proxy_ssl_header header. This is useful when behind a SSL termination proxy. """ def __init__(self, application, *args, **kwargs): removals.removed_module(__name__, "oslo_middleware.http_proxy_to_wsgi") super(SSLMiddleware, self).__init__(application, *args, **kwargs) self.oslo_conf.register_opts(OPTS, group='oslo_middleware') def process_request(self, req): self.header_name = 'HTTP_{0}'.format( self._conf_get('secure_proxy_ssl_header').upper() .replace('-', '_')) req.environ['wsgi.url_scheme'] = req.environ.get( self.header_name, req.environ['wsgi.url_scheme']) 0707010000003B000081A400000000000000000000000164F999DB0000121E000000000000000000000000000000000000003200000000oslo.middleware-2023.2.0/oslo_middleware/stats.py# 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.<appname>.<METHOD>.<path>.<from>.<url> 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) 0707010000003C000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000002F00000000oslo.middleware-2023.2.0/oslo_middleware/tests0707010000003D000081A400000000000000000000000164F999DB00000000000000000000000000000000000000000000003B00000000oslo.middleware-2023.2.0/oslo_middleware/tests/__init__.py0707010000003E000081A400000000000000000000000164F999DB00001BA7000000000000000000000000000000000000004200000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_auth_basic.py# 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)) 0707010000003F000081A400000000000000000000000164F999DB00000CBD000000000000000000000000000000000000003C00000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_base.py# 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 07070100000040000081A400000000000000000000000164F999DB00000AE7000000000000000000000000000000000000004400000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_catch_errors.py# 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) 07070100000041000081A400000000000000000000000164F999DB00000673000000000000000000000000000000000000004600000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_correlation_id.py# 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")) 07070100000042000081A400000000000000000000000164F999DB0000D89E000000000000000000000000000000000000003C00000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_cors.py# 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) 07070100000043000081A400000000000000000000000164F999DB0000057E000000000000000000000000000000000000004400000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_entry_points.py# 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', 'ssl': 'SSLMiddleware', } 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)) 07070100000044000081A400000000000000000000000164F999DB000021D7000000000000000000000000000000000000004300000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_healthcheck.py# 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): self.app = healthcheck.Healthcheck(self.application, conf) req = webob.Request.blank(path, accept=accept, method=method) req.server_port = server_port 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): res = self._do_test_request(conf=conf, path=path, accept=accept, method=method, server_port=server_port) 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'<TD>OK</TD>', 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'<TD>DISABLED BY FILE</TD>', 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') 07070100000045000081A400000000000000000000000164F999DB00001B8F000000000000000000000000000000000000004A00000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_http_proxy_to_wsgi.py# 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) 07070100000046000081A400000000000000000000000164F999DB000003B3000000000000000000000000000000000000003C00000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_opts.py# 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() def test_ssl(self): opts.list_opts_ssl() 07070100000047000081A400000000000000000000000164F999DB00000FAF000000000000000000000000000000000000004200000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_request_id.py# 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'), '') 07070100000048000081A400000000000000000000000164F999DB00000DD6000000000000000000000000000000000000004100000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_sizelimit.py# 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) 07070100000049000081A400000000000000000000000164F999DB00000847000000000000000000000000000000000000003B00000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_ssl.py# Copyright (c) 2015 Thales Services SAS # 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_config import fixture as config from oslotest import base import webob from oslo_middleware import ssl class SSLMiddlewareTest(base.BaseTestCase): def setUp(self): super(SSLMiddlewareTest, self).setUp() self.useFixture(config.Config()) def _test_scheme(self, expected, headers, secure_proxy_ssl_header=None): middleware = ssl.SSLMiddleware(None) if secure_proxy_ssl_header: middleware.oslo_conf.set_override( 'secure_proxy_ssl_header', secure_proxy_ssl_header, group='oslo_middleware') request = webob.Request.blank('http://example.com/', headers=headers) # Ensure ssl middleware does not stop pipeline execution self.assertIsNone(middleware.process_request(request)) self.assertEqual(expected, request.scheme) def test_without_forwarded_protocol(self): self._test_scheme('http', {}) def test_with_forwarded_protocol(self): headers = {'X-Forwarded-Proto': 'https'} self._test_scheme('https', headers) def test_with_custom_header(self): headers = {'X-Forwarded-Proto': 'https'} self._test_scheme('http', headers, secure_proxy_ssl_header='X-My-Header') def test_with_custom_header_and_forwarded_protocol(self): headers = {'X-My-Header': 'https'} self._test_scheme('https', headers, secure_proxy_ssl_header='X-My-Header') 0707010000004A000081A400000000000000000000000164F999DB0000134E000000000000000000000000000000000000003D00000000oslo.middleware-2023.2.0/oslo_middleware/tests/test_stats.py# 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) 0707010000004B000081A400000000000000000000000164F999DB000002B7000000000000000000000000000000000000003400000000oslo.middleware-2023.2.0/oslo_middleware/version.py# 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') 0707010000004C000041ED00000000000000000000000464F999DB00000000000000000000000000000000000000000000002600000000oslo.middleware-2023.2.0/releasenotes0707010000004D000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000002C00000000oslo.middleware-2023.2.0/releasenotes/notes0707010000004E000081A400000000000000000000000164F999DB00000039000000000000000000000000000000000000004B00000000oslo.middleware-2023.2.0/releasenotes/notes/add_reno-3b4ae0789e9c45b4.yaml--- other: - Switch to reno for managing release notes.0707010000004F000081A400000000000000000000000164F999DB00000261000000000000000000000000000000000000005800000000oslo.middleware-2023.2.0/releasenotes/notes/basic-auth-middleware-5f812399e325425f.yaml--- 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 07070100000050000081A400000000000000000000000164F999DB00000121000000000000000000000000000000000000005100000000oslo.middleware-2023.2.0/releasenotes/notes/compat_headers-55a635b8ec01b6f1.yaml--- 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. 07070100000051000081A400000000000000000000000164F999DB0000007F000000000000000000000000000000000000005800000000oslo.middleware-2023.2.0/releasenotes/notes/drop-python27-support-a6361831195bf29c.yaml--- upgrade: - | Support for Python 2.7 has been dropped. The minimum version of Python now supported is Python 3.6. 07070100000052000081A400000000000000000000000164F999DB0000013A000000000000000000000000000000000000005400000000oslo.middleware-2023.2.0/releasenotes/notes/global_request_id-a8ec7260fbd76444.yaml--- 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. 07070100000053000081A400000000000000000000000164F999DB00000041000000000000000000000000000000000000006400000000oslo.middleware-2023.2.0/releasenotes/notes/log_max_request_body_size-1835363-6f37946210a100d7.yaml--- other: - | Log when max_request_body_size is exceeded. 07070100000054000041ED00000000000000000000000564F999DB00000000000000000000000000000000000000000000002D00000000oslo.middleware-2023.2.0/releasenotes/source07070100000055000081A400000000000000000000000164F999DB00000082000000000000000000000000000000000000003800000000oslo.middleware-2023.2.0/releasenotes/source/2023.1.rst=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.1 07070100000056000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000003500000000oslo.middleware-2023.2.0/releasenotes/source/_static07070100000057000081A400000000000000000000000164F999DB00000000000000000000000000000000000000000000004200000000oslo.middleware-2023.2.0/releasenotes/source/_static/.placeholder07070100000058000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000003800000000oslo.middleware-2023.2.0/releasenotes/source/_templates07070100000059000081A400000000000000000000000164F999DB00000000000000000000000000000000000000000000004500000000oslo.middleware-2023.2.0/releasenotes/source/_templates/.placeholder0707010000005A000081A400000000000000000000000164F999DB00001D37000000000000000000000000000000000000003500000000oslo.middleware-2023.2.0/releasenotes/source/conf.py# -*- 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 # "<project> v<release> 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 <link> 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/'] 0707010000005B000081A400000000000000000000000164F999DB000000FF000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/releasenotes/source/index.rst============================= oslo.middleware Release Notes ============================= .. toctree:: :maxdepth: 1 unreleased 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata 0707010000005C000041ED00000000000000000000000464F999DB00000000000000000000000000000000000000000000003400000000oslo.middleware-2023.2.0/releasenotes/source/locale0707010000005D000041ED00000000000000000000000364F999DB00000000000000000000000000000000000000000000003A00000000oslo.middleware-2023.2.0/releasenotes/source/locale/en_GB0707010000005E000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000004600000000oslo.middleware-2023.2.0/releasenotes/source/locale/en_GB/LC_MESSAGES0707010000005F000081A400000000000000000000000164F999DB000011E5000000000000000000000000000000000000005600000000oslo.middleware-2023.2.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po# Andi Chandler <andi@gowling.com>, 2017. #zanata # Andi Chandler <andi@gowling.com>, 2018. #zanata # Andi Chandler <andi@gowling.com>, 2023. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.middleware\n" "Report-Msgid-Bugs-To: \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:57+0000\n" "Last-Translator: Andi Chandler <andi@gowling.com>\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 "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" 07070100000060000041ED00000000000000000000000364F999DB00000000000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/releasenotes/source/locale/fr07070100000061000041ED00000000000000000000000264F999DB00000000000000000000000000000000000000000000004300000000oslo.middleware-2023.2.0/releasenotes/source/locale/fr/LC_MESSAGES07070100000062000081A400000000000000000000000164F999DB0000036A000000000000000000000000000000000000005300000000oslo.middleware-2023.2.0/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po# Gérald LONLAS <g.lonlas@gmail.com>, 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 <g.lonlas@gmail.com>\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" 07070100000063000081A400000000000000000000000164F999DB00000098000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/releasenotes/source/ocata.rst=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata 07070100000064000081A400000000000000000000000164F999DB0000008F000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/releasenotes/source/pike.rst=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike 07070100000065000081A400000000000000000000000164F999DB00000093000000000000000000000000000000000000003800000000oslo.middleware-2023.2.0/releasenotes/source/queens.rst=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens 07070100000066000081A400000000000000000000000164F999DB00000091000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/releasenotes/source/rocky.rst=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky 07070100000067000081A400000000000000000000000164F999DB00000091000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/releasenotes/source/stein.rst=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein 07070100000068000081A400000000000000000000000164F999DB0000007E000000000000000000000000000000000000003700000000oslo.middleware-2023.2.0/releasenotes/source/train.rst========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train 07070100000069000081A400000000000000000000000164F999DB00000064000000000000000000000000000000000000003C00000000oslo.middleware-2023.2.0/releasenotes/source/unreleased.rst========================== Unreleased Release Notes ========================== .. release-notes:: 0707010000006A000081A400000000000000000000000164F999DB00000082000000000000000000000000000000000000003800000000oslo.middleware-2023.2.0/releasenotes/source/ussuri.rst=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri 0707010000006B000081A400000000000000000000000164F999DB0000008A000000000000000000000000000000000000003A00000000oslo.middleware-2023.2.0/releasenotes/source/victoria.rst============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria 0707010000006C000081A400000000000000000000000164F999DB00000086000000000000000000000000000000000000003900000000oslo.middleware-2023.2.0/releasenotes/source/wallaby.rst============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby 0707010000006D000081A400000000000000000000000164F999DB0000007A000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/releasenotes/source/xena.rst========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena 0707010000006E000081A400000000000000000000000164F999DB0000007A000000000000000000000000000000000000003600000000oslo.middleware-2023.2.0/releasenotes/source/yoga.rst========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: stable/yoga 0707010000006F000081A400000000000000000000000164F999DB00000076000000000000000000000000000000000000003500000000oslo.middleware-2023.2.0/releasenotes/source/zed.rst======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed 07070100000070000081A400000000000000000000000164F999DB0000021D000000000000000000000000000000000000002A00000000oslo.middleware-2023.2.0/requirements.txt# 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. pbr!=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 07070100000071000081A400000000000000000000000164F999DB000008C2000000000000000000000000000000000000002300000000oslo.middleware-2023.2.0/setup.cfg[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 :: 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.ssl = oslo_middleware.opts:list_opts_ssl 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 ssl = oslo_middleware:SSLMiddleware.factory 07070100000072000081A400000000000000000000000164F999DB000002B9000000000000000000000000000000000000002200000000oslo.middleware-2023.2.0/setup.py# 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) 07070100000073000081A400000000000000000000000164F999DB00000212000000000000000000000000000000000000002F00000000oslo.middleware-2023.2.0/test-requirements.txt# 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. fixtures>=3.0.0 # Apache-2.0/BSD hacking>=3.0.1,<3.1.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 07070100000074000081A400000000000000000000000164F999DB0000051D000000000000000000000000000000000000002100000000oslo.middleware-2023.2.0/tox.ini[tox] minversion = 3.1 envlist = py3,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2023.2} -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/2023.2} -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 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!520 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor