├── .gitignore ├── .project ├── .pydevproject ├── .twext.sublime-project ├── CONTRIBUTING.md ├── LICENSE.txt ├── PULL_REQUEST_TEMPLATE.md ├── README.rst ├── bin ├── _build.sh ├── _py.sh ├── develop ├── environment ├── gendocs ├── pyflakes ├── python ├── test ├── test_opendirectory_auth ├── test_opendirectory_lookup ├── trial ├── twistd └── update_copyrights ├── requirements-dev.txt ├── setup.py ├── twext ├── __init__.py ├── application │ ├── __init__.py │ ├── masterchild.py │ └── service.py ├── enterprise │ ├── __init__.py │ ├── adbapi2.py │ ├── dal │ │ ├── __init__.py │ │ ├── model.py │ │ ├── parseschema.py │ │ ├── record.py │ │ ├── syntax.py │ │ └── test │ │ │ ├── __init__.py │ │ │ ├── test_parseschema.py │ │ │ ├── test_record.py │ │ │ └── test_sqlsyntax.py │ ├── fixtures.py │ ├── ienterprise.py │ ├── jobs │ │ ├── __init__.py │ │ ├── jobitem.py │ │ ├── queue.py │ │ ├── test │ │ │ ├── __init__.py │ │ │ └── test_jobs.py │ │ ├── utils.py │ │ └── workitem.py │ ├── locking.py │ ├── queue.py │ ├── test │ │ ├── __init__.py │ │ ├── test_adbapi2.py │ │ ├── test_fixtures.py │ │ ├── test_locking.py │ │ ├── test_queue.py │ │ └── test_util.py │ └── util.py ├── internet │ ├── __init__.py │ ├── adaptendpoint.py │ ├── decorate.py │ ├── fswatch.py │ ├── gaiendpoint.py │ ├── sendfdport.py │ ├── socketfile.py │ ├── spawnsvc.py │ ├── ssl.py │ ├── tcp.py │ ├── test │ │ ├── __init__.py │ │ ├── test_adaptendpoint.py │ │ ├── test_fswatch.py │ │ ├── test_gaiendpoint.py │ │ └── test_sendfdport.py │ └── threadutils.py ├── protocols │ ├── __init__.py │ ├── echo.py │ └── test │ │ └── __init__.py ├── python │ ├── __init__.py │ ├── clsprop.py │ ├── filepath.py │ ├── launchd.py │ ├── log.py │ ├── parallel.py │ ├── sacl.py │ ├── sendfd.py │ ├── test │ │ ├── __init__.py │ │ ├── pullpipe.py │ │ ├── test_filepath.py │ │ ├── test_launchd.py │ │ ├── test_log.py │ │ └── test_parallel.py │ ├── types.py │ └── usage.py └── who │ ├── __init__.py │ ├── aggregate.py │ ├── checker.py │ ├── directory.py │ ├── expression.py │ ├── idirectory.py │ ├── index.py │ ├── ldap │ ├── __init__.py │ ├── _constants.py │ ├── _service.py │ ├── _util.py │ └── test │ │ ├── __init__.py │ │ ├── test_service.py │ │ └── test_util.py │ ├── opendirectory │ ├── __init__.py │ ├── _constants.py │ ├── _odframework.py │ ├── _scripts.py │ ├── _service.py │ └── test │ │ ├── __init__.py │ │ └── test_service.py │ ├── test │ ├── __init__.py │ ├── auth_resource.rpy │ ├── test_aggregate.py │ ├── test_concurrency.py │ ├── test_directory.py │ ├── test_expression.py │ ├── test_index.py │ ├── test_util.py │ └── test_xml.py │ ├── util.py │ └── xml.py └── twisted └── plugins └── masterchild.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.o 3 | *.py[co] 4 | *.so 5 | _trial_temp*/ 6 | build/ 7 | dropin.cache 8 | *~ 9 | *.lock 10 | .develop/ 11 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | twextpy 4 | 5 | 6 | Twisted 7 | 8 | 9 | 10 | org.python.pydev.PyDevBuilder 11 | 12 | 13 | 14 | 15 | 16 | org.python.pydev.pythonNature 17 | 18 | 19 | 20 | 0 21 | 22 | 10 23 | 24 | org.eclipse.ui.ide.multiFilter 25 | 1.0-projectRelativePath-matches-true-false-.develop 26 | 27 | 28 | 29 | 0 30 | 31 | 10 32 | 33 | org.eclipse.ui.ide.multiFilter 34 | 1.0-projectRelativePath-matches-false-false-build 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | python 2.7 4 | Default 5 | 6 | /${PROJECT_DIR_NAME} 7 | 8 | 9 | -------------------------------------------------------------------------------- /.twext.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "build_systems": 3 | [ 4 | { 5 | "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)", 6 | "name": "Anaconda Python Builder", 7 | "selector": "source.python", 8 | "shell_cmd": "\"./bin/python\" -u \"$file\"" 9 | }, 10 | { 11 | "cmd": 12 | [ 13 | "${project_path}/bin/trial", 14 | "--temp-directory=${project_path}/.develop/trial", 15 | "--random=0", 16 | "--testmodule=${file}" 17 | ], 18 | "name": "Trial", 19 | "selector": "source.python" 20 | } 21 | ], 22 | "folders": 23 | [ 24 | { 25 | "file_exclude_patterns": 26 | [ 27 | ".project", 28 | ".pydevproject", 29 | "*.pyc", 30 | "*.pyo" 31 | ], 32 | "folder_exclude_patterns": 33 | [ 34 | ".git", 35 | ".develop", 36 | "*.egg-info" 37 | ], 38 | "name": "twext", 39 | "path": "." 40 | } 41 | ], 42 | "settings": 43 | { 44 | "pep257": true, 45 | "pep257_ignore": 46 | [ 47 | "D102", 48 | "D105", 49 | "D200", 50 | "D203", 51 | "D205", 52 | "D400" 53 | ], 54 | "pep8_ignore": 55 | [ 56 | "E202", 57 | "E203", 58 | "E221", 59 | "E302", 60 | "E303", 61 | "E402" 62 | ], 63 | "pep8_max_line_length": 80, 64 | "pyflakes_ignore_import_*": false, 65 | "python_interpreter": "./bin/python", 66 | "tab_size": 4, 67 | "test_command": "./bin/trial", 68 | "test_delimeter": ".", 69 | "test_project_path": "twext" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting a request, you represent that you have the right to license 2 | your contribution to Apple and the community, and agree that your 3 | contributions are licensed under the [Apache License Version 2.0](LICENSE.txt). 4 | 5 | For existing files modified by your request, you represent that you have 6 | retained any existing copyright notices and licensing terms. For each new 7 | file in your request, you represent that you have added to the file a 8 | copyright notice (including the year and the copyright owner's name) and the 9 | Calendar and Contacts Server's licensing terms. 10 | 11 | Before submitting the request, please make sure that your request follows 12 | the [Calendar and Contacts Server's guidelines for contributing 13 | code](../../../ccs-calendarserver/blob/master/HACKING.rst). 14 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | By submitting a request, you represent that you have the right to license 2 | your contribution to Apple and the community, and agree that your 3 | contributions are licensed under the [Apache License Version 2.0](LICENSE.txt). 4 | 5 | For existing files modified by your request, you represent that you have 6 | retained any existing copyright notices and licensing terms. For each new 7 | file in your request, you represent that you have added to the file a 8 | copyright notice (including the year and the copyright owner's name) and the 9 | Calendar and Contacts Server's licensing terms. 10 | 11 | Before submitting the request, please make sure that your request follows 12 | the [Calendar and Contacts Server's guidelines for contributing 13 | code](../../../ccs-calendarserver/blob/master/HACKING.rst). 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | This is a python module consisting of extensions to the Twisted Framework 5 | (http://twistedmatrix.com/). 6 | 7 | 8 | Copyright and License 9 | ===================== 10 | 11 | Copyright (c) 2005-2017 Apple Inc. All rights reserved. 12 | 13 | This software is licensed under the Apache License, Version 2.0. 14 | 15 | See the file LICENSE_ for the full text of the license terms. 16 | 17 | .. _LICENSE: LICENSE.txt 18 | 19 | 20 | Installation 21 | ============ 22 | 23 | Python version 2.7 is supported. 24 | 25 | This library is can be built using the standard distutils mechanisms. 26 | 27 | It is also registered with the Python Package Index (PyPI) as ``twextpy`` 28 | (the name ``twext`` is used by another module in PyPI) for use with ``pip`` and 29 | ``easy_install``:: 30 | 31 | pip install twextpy 32 | 33 | This will build and install the ``twext`` module along with its base 34 | dependencies. This library has a number of optional features which must be 35 | specified in order to download build and install their dependencies, for 36 | example:: 37 | 38 | pip install twextpy[DAL,Postgres] 39 | 40 | These features are: 41 | 42 | DAL 43 | Enables use of the Database Abstraction Layer implemented in 44 | ``twext.enterprise.dal``. 45 | 46 | LDAP 47 | Enables support for the Lightweight Directory Access Protocol in 48 | ``twext.who.ldap``. 49 | 50 | OpenDirectory 51 | Enables support for the (Mac OS) OpenDirectory framework in 52 | ``twext.who.opendirectory``. 53 | 54 | Oracle 55 | Enables support for Oracle database connectivity in ``twext.enterprise`` and 56 | Oracle syntax in ``twext.enterprise.dal``. 57 | 58 | Postgres 59 | Enables support for Postgres database connectivity in ``twext.enterprise``. 60 | 61 | 62 | Development 63 | =========== 64 | 65 | If you are planning to work on this library, you can manage this with standard 66 | distutils mechanisms. There are, however, some tools in the ``bin`` directory 67 | which automate this management for you. 68 | 69 | To use these tools, you must have ``pip`` on your system. 70 | If you do not have ``pip``, instructions for installing it are at 71 | http://www.pip-installer.org/en/latest/installing.html. 72 | The script ``install_pip`` (requires ``sudo`` access) automates this for you. 73 | 74 | The shell script ``develop`` downloads and builds any dependancies and sets up a 75 | development environment. ``develop`` handles non-Python as well as Python 76 | dependancies. 77 | 78 | The tools ``python``, ``pyflakes``, ``trial``, and ``twistd`` are wrappers 79 | around the cooresponding commands that use ``develop`` to ensure that 80 | dependancies are available, ``PYTHONPATH`` is set up correctly, etc. 81 | 82 | ``test`` runs all of the unit tests and does linting. It should be run before 83 | checking in any code. 84 | -------------------------------------------------------------------------------- /bin/_py.sh: -------------------------------------------------------------------------------- 1 | # -*- sh-basic-offset: 2 -*- 2 | ## 3 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | find_cmd () { 19 | local cmd="$1"; shift; 20 | 21 | local path="$(type "${cmd}" 2>/dev/null | sed "s|^${cmd} is \(a tracked alias for \)\{0,1\}||")"; 22 | 23 | if [ -z "${cmd}" ]; then 24 | return 1; 25 | fi; 26 | 27 | echo "${path}"; 28 | } 29 | 30 | # Echo the major.minor version of the given Python interpreter. 31 | 32 | py_version () { 33 | local python="$1"; shift; 34 | echo "$("${python}" -c "from distutils.sysconfig import get_python_version; print get_python_version()")"; 35 | } 36 | 37 | # 38 | # Test if a particular python interpreter is available, given the full path to 39 | # that interpreter. 40 | # 41 | try_python () { 42 | local python="$1"; shift; 43 | 44 | if [ -z "${python}" ]; then 45 | return 1; 46 | fi; 47 | 48 | if ! type "${python}" > /dev/null 2>&1; then 49 | return 1; 50 | fi; 51 | 52 | local py_version="$(py_version "${python}")"; 53 | if [ "$(echo "${py_version}" | sed 's|\.||')" -lt "25" ]; then 54 | return 1; 55 | fi; 56 | return 0; 57 | } 58 | 59 | 60 | # 61 | # Detect which version of Python to use, then print out which one was detected. 62 | # 63 | # This will prefer the python interpreter in the PYTHON environment variable. 64 | # If that's not found, it will check for "python2.7" and "python", looking for 65 | # each in your PATH. 66 | # 67 | detect_python_version () { 68 | local v; 69 | local p; 70 | for v in "2.7" "" 71 | do 72 | for p in \ 73 | "${PYTHON:=}" \ 74 | "python${v}" \ 75 | ; 76 | do 77 | if p="$(find_cmd "${p}")"; then 78 | if try_python "${p}"; then 79 | echo "${p}"; 80 | return 0; 81 | fi; 82 | fi; 83 | done; 84 | done; 85 | return 1; 86 | } 87 | 88 | 89 | # 90 | # Compare version numbers 91 | # 92 | cmp_version () { 93 | local v="$1"; shift; 94 | local mv="$1"; shift; 95 | 96 | local vh; 97 | local mvh; 98 | local result; 99 | 100 | while true; do 101 | vh="${v%%.*}"; # Get highest-order segment 102 | mvh="${mv%%.*}"; 103 | 104 | if [ "${vh}" -gt "${mvh}" ]; then 105 | result=1; 106 | break; 107 | fi; 108 | 109 | if [ "${vh}" -lt "${mvh}" ]; then 110 | result=0; 111 | break; 112 | fi; 113 | 114 | if [ "${v}" = "${v#*.}" ]; then 115 | # No dots left, so we're ok 116 | result=0; 117 | break; 118 | fi; 119 | 120 | if [ "${mv}" = "${mv#*.}" ]; then 121 | # No dots left, so we're not gonna match 122 | result=1; 123 | break; 124 | fi; 125 | 126 | v="${v#*.}"; 127 | mv="${mv#*.}"; 128 | done; 129 | 130 | return ${result}; 131 | } 132 | 133 | 134 | # 135 | # Detect which python to use, and store it in the 'python' variable, as well as 136 | # setting up variables related to version and build configuration. 137 | # 138 | init_py () { 139 | # First, detect the appropriate version of Python to use, based on our version 140 | # requirements and the environment. Note that all invocations of python in 141 | # our build scripts should therefore be '"${python}"', not 'python'; this is 142 | # important on systems with older system pythons (2.4 or earlier) with an 143 | # alternate install of Python, or alternate python installation mechanisms 144 | # like virtualenv. 145 | bootstrap_python="$(detect_python_version)"; 146 | 147 | # Set the $PYTHON environment variable to an absolute path pointing at the 148 | # appropriate python executable, a standard-ish mechanism used by certain 149 | # non-distutils things that need to find the "right" python. For instance, 150 | # the part of the PostgreSQL build process which builds pl_python. Note that 151 | # detect_python_version, above, already honors $PYTHON, so if this is already 152 | # set it won't be stomped on, it will just be re-set to the same value. 153 | export PYTHON="$(find_cmd ${bootstrap_python})"; 154 | 155 | if [ -z "${bootstrap_python:-}" ]; then 156 | echo "No suitable python found. Python 2.6 or 2.7 is required."; 157 | exit 1; 158 | fi; 159 | } 160 | -------------------------------------------------------------------------------- /bin/develop: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e; 21 | set -u; 22 | 23 | if [ -z "${wd:-}" ]; then 24 | wd="$(cd "$(dirname "$0")/.." && pwd)"; 25 | fi; 26 | 27 | . "${wd}/bin/_build.sh"; 28 | 29 | develop; 30 | -------------------------------------------------------------------------------- /bin/environment: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e 21 | set -u 22 | 23 | wd="$(cd "$(dirname "$0")/.." && pwd)"; 24 | 25 | . "${wd}/bin/_build.sh"; 26 | 27 | do_setup="false"; 28 | 29 | develop > /dev/null; 30 | 31 | if [ $# -gt 0 ]; then 32 | for name in "$@"; do 33 | # Use python to avoid using eval. 34 | "${python}" -c 'import os; print os.environ.get("'${name}'", "")'; 35 | done; 36 | else 37 | env; 38 | fi; 39 | -------------------------------------------------------------------------------- /bin/gendocs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2014-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e; 21 | set -u; 22 | 23 | wd="$(cd "$(dirname "$0")/.." && pwd -L)"; 24 | 25 | export TWEXT_DEVELOP="true"; 26 | 27 | . "${wd}/bin/_build.sh"; 28 | 29 | init_build > /dev/null; 30 | py_dependencies || true; 31 | 32 | echo "Installing/updating pydoctor..."; 33 | "${python}" -m pip install twisted nevow pydoctor --upgrade; 34 | 35 | pydoctor="${py_bindir}/pydoctor"; 36 | 37 | "${pydoctor}" \ 38 | --project-name twext \ 39 | --project-url "$(setup_print url)" \ 40 | --system-class pydoctor.twistedmodel.TwistedSystem \ 41 | --project-base-dir "${wd}" \ 42 | --add-package "${wd}/twext" \ 43 | --html-output "${wd}/docs/api" \ 44 | --html-write-function-pages \ 45 | --make-html \ 46 | ; 47 | 48 | # --quiet 49 | -------------------------------------------------------------------------------- /bin/pyflakes: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e; 21 | set -u; 22 | 23 | wd="$(cd "$(dirname "$0")/.." && pwd -L)"; 24 | 25 | . "${wd}/bin/_build.sh"; 26 | 27 | init_build > /dev/null; 28 | 29 | if [ $# -eq 0 ]; then 30 | set - calendarserver contrib twisted twistedcaldav txdav txweb2; 31 | fi; 32 | 33 | echo "Checking modules:" "$@"; 34 | exec "${python}" -m pyflakes "$@"; 35 | -------------------------------------------------------------------------------- /bin/python: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e 21 | set -u 22 | 23 | wd="$(cd "$(dirname "$0")/.." && pwd)"; 24 | 25 | . "${wd}/bin/_build.sh"; 26 | 27 | do_setup="false"; 28 | 29 | develop > /dev/null; 30 | 31 | exec "${python}" "$@"; 32 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e; 21 | set -u; 22 | 23 | # 24 | # Initialize build support 25 | # 26 | 27 | wd="$(cd "$(dirname "$0")/.." && pwd -L)"; 28 | 29 | . "${wd}/bin/_build.sh"; 30 | 31 | init_build > /dev/null; 32 | 33 | 34 | 35 | # 36 | # Options 37 | # 38 | 39 | do_setup="false"; 40 | do_get="false"; 41 | 42 | random="--random=$(date "+%s")"; 43 | no_color=""; 44 | until_fail=""; 45 | coverage=""; 46 | numjobs=""; 47 | reactor=""; 48 | 49 | if [ "$(uname -s)" == "Darwin" ]; then 50 | reactor="--reactor=kqueue"; 51 | fi; 52 | 53 | usage () 54 | { 55 | program="$(basename "$0")"; 56 | 57 | if [ "${1--}" != "-" ]; then echo "$@"; echo; fi; 58 | 59 | echo "Usage: ${program} [options]"; 60 | echo "Options:"; 61 | echo " -h Print this help and exit"; 62 | echo " -n Do not use color"; 63 | echo " -o Do not run tests in random order."; 64 | echo " -r Use specified seed to determine order."; 65 | echo " -u Run until the tests fail."; 66 | echo " -c Generate coverage reports."; 67 | 68 | if [ "${1-}" == "-" ]; then return 0; fi; 69 | exit 64; 70 | } 71 | 72 | while getopts "nhoucr:j:" option; do 73 | case "${option}" in 74 | '?') usage; ;; 75 | 'h') usage -; exit 0; ;; 76 | 'o') random=""; ;; 77 | 'r') random="--random=$OPTARG"; ;; 78 | 'n') no_color="--reporter=bwverbose"; ;; 79 | 'u') until_fail="--until-failure"; ;; 80 | 'c') coverage="--coverage"; ;; 81 | 'j') numjobs="-j $OPTARG"; ;; 82 | esac; 83 | done; 84 | shift $((${OPTIND} - 1)); 85 | 86 | if [ $# -eq 0 ]; then 87 | lint="true"; 88 | set - twext; 89 | else 90 | lint="false"; 91 | fi; 92 | 93 | 94 | 95 | # 96 | # Dependencies 97 | # 98 | 99 | c_dependencies >> "${dev_home}/setup.log"; 100 | py_dependencies >> "${dev_home}/setup.log"; 101 | 102 | 103 | 104 | # 105 | # Clean up 106 | # 107 | 108 | find "${wd}" -name \*.pyc -print0 | xargs -0 rm; 109 | 110 | 111 | 112 | # 113 | # Unit tests 114 | # 115 | 116 | cd "${wd}" && \ 117 | "${wd}/bin/trial" \ 118 | --temp-directory="${dev_home}/trial" \ 119 | --rterrors \ 120 | ${reactor} \ 121 | ${random} \ 122 | ${until_fail} \ 123 | ${no_color} \ 124 | ${coverage} \ 125 | ${numjobs} \ 126 | "$@"; 127 | 128 | 129 | if ! "${lint}"; then 130 | exit 0; 131 | fi; 132 | 133 | 134 | 135 | # 136 | # Linting 137 | # 138 | 139 | echo ""; 140 | echo "Running pyflakes..."; 141 | 142 | "${python}" -m pip install pyflakes --upgrade >> "${dev_home}/setup.log"; 143 | tmp="$(mktemp -t "twext_flakes.XXXXX")"; 144 | cd "${wd}" && "${python}" -m pyflakes "$@" | tee "${tmp}" 2>&1; 145 | if [ -s "${tmp}" ]; then 146 | echo "**** Pyflakes says you have some code to clean up. ****"; 147 | exit 1; 148 | fi; 149 | rm -f "${tmp}"; 150 | 151 | 152 | 153 | # 154 | # Empty files 155 | # 156 | 157 | echo ""; 158 | echo "Checking for empty files..."; 159 | tmp="$(mktemp -t "twext_test_empty.XXXXX")"; 160 | 161 | find "${wd}" \ 162 | '!' '(' \ 163 | -type d \ 164 | '(' -path '*/.*' -or -name data -or -name build ')' \ 165 | -prune \ 166 | ')' \ 167 | -type f -size 0 \ 168 | > "${tmp}"; 169 | 170 | if [ -s "${tmp}" ]; then 171 | echo "**** Empty files: ****"; 172 | cat "${tmp}"; 173 | exit 1; 174 | fi; 175 | rm -f "${tmp}"; 176 | -------------------------------------------------------------------------------- /bin/test_opendirectory_auth: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## 4 | # Copyright (c) 2006-2017 Apple Inc. All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ## 18 | 19 | from __future__ import print_function 20 | 21 | import sys 22 | 23 | # PYTHONPATH 24 | 25 | if __name__ == "__main__": 26 | if "PYTHONPATH" in globals(): 27 | sys.path.insert(0, PYTHONPATH) 28 | else: 29 | try: 30 | import _twext_preamble 31 | except ImportError: 32 | sys.exc_clear() 33 | 34 | from twext.who.opendirectory._scripts import run_auth 35 | run_auth() 36 | -------------------------------------------------------------------------------- /bin/test_opendirectory_lookup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## 4 | # Copyright (c) 2006-2017 Apple Inc. All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ## 18 | 19 | from __future__ import print_function 20 | 21 | import sys 22 | 23 | # PYTHONPATH 24 | 25 | if __name__ == "__main__": 26 | if "PYTHONPATH" in globals(): 27 | sys.path.insert(0, PYTHONPATH) 28 | else: 29 | try: 30 | import _twext_preamble 31 | except ImportError: 32 | sys.exc_clear() 33 | 34 | from twext.who.opendirectory._scripts import run_lookup 35 | run_lookup() 36 | -------------------------------------------------------------------------------- /bin/trial: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e 21 | set -u 22 | 23 | wd="$(cd "$(dirname "$0")/.." && pwd)"; 24 | 25 | . "${wd}/bin/_build.sh"; 26 | 27 | init_build > /dev/null; 28 | do_setup="false"; 29 | develop > /dev/null; 30 | 31 | exec "${python}" \ 32 | -c "\ 33 | import sys;\ 34 | import os;\ 35 | sys.path.insert(0, os.path.abspath(os.getcwd()));\ 36 | from twisted.scripts.trial import run;\ 37 | run()\ 38 | " \ 39 | "$@"; 40 | -------------------------------------------------------------------------------- /bin/twistd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e 21 | set -u 22 | 23 | wd="$(cd "$(dirname "$0")/.." && pwd)"; 24 | 25 | . "${wd}/bin/_build.sh"; 26 | 27 | init_build > /dev/null; 28 | 29 | exec "${python}" \ 30 | -c "\ 31 | import sys;\ 32 | import os;\ 33 | sys.path.insert(0, os.path.abspath(os.getcwd()));\ 34 | from twisted.scripts.twistd import run;\ 35 | run()\ 36 | " \ 37 | "$@"; 38 | -------------------------------------------------------------------------------- /bin/update_copyrights: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- sh-basic-offset: 2 -*- 3 | 4 | ## 5 | # Copyright (c) 2013-2016 Apple Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ## 19 | 20 | set -e; 21 | set -u; 22 | 23 | find_files () { 24 | where="$1"; shift; 25 | 26 | find "${where}" \ 27 | ! \( \ 28 | -type d \ 29 | \( \ 30 | -name .git -o \ 31 | -name build -o \ 32 | -name data -o \ 33 | -name '_trial_temp*' \ 34 | \) \ 35 | -prune \ 36 | \) \ 37 | -type f \ 38 | ! -name '.#*' \ 39 | ! -name '#*#' \ 40 | ! -name '*~' \ 41 | ! -name '*.pyc' \ 42 | ! -name '*.log' \ 43 | ! -name update_copyrights \ 44 | -print0; 45 | } 46 | 47 | wd="$(cd "$(dirname "$0")/.." && pwd)"; 48 | 49 | this_year="$(date "+%Y")"; 50 | last_year=$((${this_year} - 1)); 51 | 52 | tmp="$(mktemp -t "$$")"; 53 | find_files "${wd}" > "${tmp}"; 54 | 55 | ff () { cat "${tmp}"; } 56 | 57 | echo "Updating copyrights from ${last_year} to ${this_year}..."; 58 | 59 | ff | xargs -0 perl -i -pe 's|(Copyright \(c\) .*-)'"${last_year}"'( Apple)|${1}'"${this_year}"'${2}|'; 60 | ff | xargs -0 perl -i -pe 's|(Copyright \(c\) )'"${last_year}"'( Apple)|${1}'"${last_year}-${this_year}"'${2}|'; 61 | 62 | ff | xargs -0 grep -e 'Copyright (c) .* Apple' \ 63 | | grep -v -e 'Copyright (c) .*'"${this_year}"' Apple' \ 64 | ; 65 | 66 | rm "${tmp}"; 67 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Requirements for development. 2 | 3 | # Get our own dependencies (from setup.py) 4 | -e . 5 | 6 | # Additional dependencies for development 7 | pyflakes 8 | docutils>=0.11 9 | mockldap>=0.1.4 10 | -------------------------------------------------------------------------------- /twext/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to the Twisted Framework. 19 | """ 20 | 21 | try: 22 | from twext.version import version as __version__ 23 | except ImportError: 24 | __version__ = None 25 | -------------------------------------------------------------------------------- /twext/application/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to L{twisted.application}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/application/service.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to L{twisted.application.service}. 19 | """ 20 | 21 | __all__ = [ 22 | "ReExecService", 23 | ] 24 | 25 | import sys 26 | import os 27 | import signal 28 | 29 | from twisted.application.service import MultiService 30 | 31 | from twext.python.log import Logger 32 | 33 | 34 | class ReExecService(MultiService): 35 | """ 36 | A MultiService which catches SIGHUP and re-exec's the process. 37 | """ 38 | log = Logger() 39 | 40 | def __init__(self, pidfilePath, reactor=None): 41 | """ 42 | @param pidFilePath: Absolute path to the pidfile which will need to be 43 | removed 44 | @type pidFilePath: C{str} 45 | """ 46 | self.pidfilePath = pidfilePath 47 | if reactor is None: 48 | from twisted.internet import reactor 49 | self.reactor = reactor 50 | MultiService.__init__(self) 51 | 52 | def reExec(self): 53 | """ 54 | Removes pidfile, registers an exec to happen after shutdown, then 55 | stops the reactor. 56 | """ 57 | self.log.warn("SIGHUP received - restarting") 58 | try: 59 | self.log.info("Removing pidfile: {log_source.pidfilePath}") 60 | os.remove(self.pidfilePath) 61 | except OSError: 62 | pass 63 | self.reactor.addSystemEventTrigger( 64 | "after", "shutdown", os.execv, 65 | sys.executable, [sys.executable] + sys.argv 66 | ) 67 | self.reactor.stop() 68 | 69 | def sighupHandler(self, num, frame): 70 | self.reactor.callFromThread(self.reExec) 71 | 72 | def startService(self): 73 | self.previousHandler = signal.signal(signal.SIGHUP, self.sighupHandler) 74 | MultiService.startService(self) 75 | 76 | def stopService(self): 77 | signal.signal(signal.SIGHUP, self.previousHandler) 78 | MultiService.stopService(self) 79 | -------------------------------------------------------------------------------- /twext/enterprise/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to L{twisted.enterprise}; 19 | things related to database connectivity and management. 20 | """ 21 | -------------------------------------------------------------------------------- /twext/enterprise/dal/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Toolkit for building a Data-Access Layer (DAL). 19 | 20 | This includes an abstract representation of SQL objects like tables, columns, 21 | sequences and queries, a parser to convert your schema to that representation, 22 | and tools for working with it. 23 | 24 | In some ways this is similar to the low levels of something like SQLAlchemy, but 25 | it is designed to be more introspectable, to allow for features like automatic 26 | caching and index detection. NB: work in progress. 27 | """ 28 | -------------------------------------------------------------------------------- /twext/enterprise/dal/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.enterprise.dal}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/enterprise/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2015-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | -------------------------------------------------------------------------------- /twext/enterprise/jobs/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2015-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | -------------------------------------------------------------------------------- /twext/enterprise/jobs/utils.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2015-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | from twisted.internet.defer import inlineCallbacks, returnValue 18 | from twisted.python.failure import Failure 19 | from datetime import datetime 20 | 21 | 22 | @inlineCallbacks 23 | def inTransaction(transactionCreator, operation, label="jobqueue.inTransaction", **kwargs): 24 | """ 25 | Perform the given operation in a transaction, committing or aborting as 26 | required. 27 | 28 | @param transactionCreator: a 0-arg callable that returns an 29 | L{IAsyncTransaction} 30 | 31 | @param operation: a 1-arg callable that takes an L{IAsyncTransaction} and 32 | returns a value. 33 | 34 | @param label: label to be used with the transaction. 35 | 36 | @return: a L{Deferred} that fires with C{operation}'s result or fails with 37 | its error, unless there is an error creating, aborting or committing 38 | the transaction. 39 | """ 40 | txn = transactionCreator(label=label) 41 | try: 42 | result = yield operation(txn, **kwargs) 43 | except: 44 | f = Failure() 45 | yield txn.abort() 46 | returnValue(f) 47 | else: 48 | yield txn.commit() 49 | returnValue(result) 50 | 51 | 52 | def astimestamp(v): 53 | """ 54 | Convert the given datetime to a POSIX timestamp. 55 | """ 56 | return (v - datetime.utcfromtimestamp(0)).total_seconds() 57 | -------------------------------------------------------------------------------- /twext/enterprise/locking.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.enterprise.test.test_locking -*- 2 | ## 3 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Utilities to restrict concurrency based on mutual exclusion. 20 | """ 21 | 22 | from twext.enterprise.dal.model import Table 23 | from twext.enterprise.dal.model import SQLType 24 | from twext.enterprise.dal.model import Constraint 25 | from twext.enterprise.dal.syntax import SchemaSyntax 26 | from twext.enterprise.dal.model import Schema 27 | from twext.enterprise.dal.record import Record 28 | from twext.enterprise.dal.record import fromTable 29 | 30 | 31 | class AlreadyUnlocked(Exception): 32 | """ 33 | The lock you were trying to unlock was already unlocked. 34 | """ 35 | 36 | 37 | class LockTimeout(Exception): 38 | """ 39 | The lock you were trying to lock was already locked causing a timeout. 40 | """ 41 | 42 | 43 | def makeLockSchema(inSchema): 44 | """ 45 | Create a self-contained schema just for L{Locker} use, in C{inSchema}. 46 | 47 | @param inSchema: a L{Schema} to add the locks table to. 48 | @type inSchema: L{Schema} 49 | 50 | @return: inSchema 51 | """ 52 | LockTable = Table(inSchema, "NAMED_LOCK") 53 | 54 | LockTable.addColumn("LOCK_NAME", SQLType("varchar", 255)) 55 | LockTable.tableConstraint(Constraint.NOT_NULL, ["LOCK_NAME"]) 56 | LockTable.tableConstraint(Constraint.UNIQUE, ["LOCK_NAME"]) 57 | LockTable.primaryKey = [LockTable.columnNamed("LOCK_NAME")] 58 | 59 | return inSchema 60 | 61 | LockSchema = SchemaSyntax(makeLockSchema(Schema(__file__))) 62 | 63 | 64 | class NamedLock(Record, fromTable(LockSchema.NAMED_LOCK)): 65 | """ 66 | An L{AcquiredLock} lock against a shared data store that the current 67 | process holds via the referenced transaction. 68 | """ 69 | 70 | @classmethod 71 | def acquire(cls, txn, name): 72 | """ 73 | Acquire a lock with the given name. 74 | 75 | @param name: The name of the lock to acquire. Against the same store, 76 | no two locks may be acquired. 77 | @type name: L{unicode} 78 | 79 | @return: a L{Deferred} that fires with an L{AcquiredLock} when the lock 80 | has fired, or fails when the lock has not been acquired. 81 | """ 82 | 83 | def autoRelease(self): 84 | txn.preCommit(lambda: self.release(True)) 85 | return self 86 | 87 | def lockFailed(f): 88 | raise LockTimeout(name) 89 | 90 | d = cls.create(txn, lockName=name) 91 | d.addCallback(autoRelease) 92 | d.addErrback(lockFailed) 93 | return d 94 | 95 | def release(self, ignoreAlreadyUnlocked=False): 96 | """ 97 | Release this lock. 98 | 99 | @param ignoreAlreadyUnlocked: If you don't care about the current 100 | status of this lock, and just want to release it if it is still 101 | acquired, pass this parameter as L{True}. Otherwise this method 102 | will raise an exception if it is invoked when the lock has already 103 | been released. 104 | 105 | @raise: L{AlreadyUnlocked} 106 | 107 | @return: A L{Deferred} that fires with L{None} when the lock has been 108 | unlocked. 109 | """ 110 | return self.delete() 111 | -------------------------------------------------------------------------------- /twext/enterprise/test/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Tests for L{twext.enterprise}. 20 | """ 21 | -------------------------------------------------------------------------------- /twext/enterprise/test/test_fixtures.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.enterprise.fixtures}. 19 | 20 | Quis custodiet ipsos custodes? This module, that's who. 21 | """ 22 | 23 | from twext.enterprise.fixtures import buildConnectionPool 24 | 25 | from twisted.trial.unittest import TestCase 26 | from twisted.trial.reporter import TestResult 27 | from twext.enterprise.adbapi2 import ConnectionPool 28 | 29 | 30 | class PoolTests(TestCase): 31 | """ 32 | Tests for fixtures that create a connection pool. 33 | """ 34 | 35 | def test_buildConnectionPool(self): 36 | """ 37 | L{buildConnectionPool} returns a L{ConnectionPool} which will be 38 | running only for the duration of the test. 39 | """ 40 | collect = [] 41 | 42 | class SampleTest(TestCase): 43 | 44 | def setUp(self): 45 | self.pool = buildConnectionPool(self) 46 | 47 | def test_sample(self): 48 | collect.append(self.pool.running) 49 | 50 | def tearDown(self): 51 | collect.append(self.pool.running) 52 | 53 | r = TestResult() 54 | t = SampleTest("test_sample") 55 | t.run(r) 56 | self.assertIsInstance(t.pool, ConnectionPool) 57 | self.assertEqual([True, False], collect) 58 | -------------------------------------------------------------------------------- /twext/enterprise/test/test_locking.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for mutual exclusion locks. 19 | """ 20 | 21 | from twisted.internet.defer import inlineCallbacks 22 | from twisted.trial.unittest import TestCase 23 | 24 | from twext.enterprise.fixtures import buildConnectionPool 25 | from twext.enterprise.locking import NamedLock, LockTimeout 26 | from twext.enterprise.dal.syntax import Select 27 | from twext.enterprise.locking import LockSchema 28 | 29 | schemaText = """ 30 | create table NAMED_LOCK (LOCK_NAME varchar(255) unique primary key); 31 | """ 32 | 33 | 34 | class TestLocking(TestCase): 35 | """ 36 | Test locking and unlocking a database row. 37 | """ 38 | 39 | def setUp(self): 40 | """ 41 | Build a connection pool for the tests to use. 42 | """ 43 | self.pool = buildConnectionPool(self, schemaText) 44 | 45 | @inlineCallbacks 46 | def test_acquire(self): 47 | """ 48 | Acquiring a lock adds a row in that transaction. 49 | """ 50 | txn = self.pool.connection() 51 | yield NamedLock.acquire(txn, u"a test lock") 52 | rows = yield Select(From=LockSchema.NAMED_LOCK).on(txn) 53 | self.assertEquals(rows, [tuple([u"a test lock"])]) 54 | 55 | @inlineCallbacks 56 | def test_release(self): 57 | """ 58 | Releasing an acquired lock removes the row. 59 | """ 60 | txn = self.pool.connection() 61 | lck = yield NamedLock.acquire(txn, u"a test lock") 62 | yield lck.release() 63 | rows = yield Select(From=LockSchema.NAMED_LOCK).on(txn) 64 | self.assertEquals(rows, []) 65 | 66 | @inlineCallbacks 67 | def test_autoRelease(self): 68 | """ 69 | Committing a transaction automatically releases all of its locks. 70 | """ 71 | txn = self.pool.connection() 72 | yield NamedLock.acquire(txn, u"something") 73 | yield txn.commit() 74 | txn2 = self.pool.connection() 75 | rows = yield Select(From=LockSchema.NAMED_LOCK).on(txn2) 76 | self.assertEquals(rows, []) 77 | 78 | @inlineCallbacks 79 | def test_timeout(self): 80 | """ 81 | Trying to acquire second lock times out. 82 | """ 83 | txn1 = self.pool.connection() 84 | yield NamedLock.acquire(txn1, u"a test lock") 85 | 86 | txn2 = self.pool.connection() 87 | yield self.assertFailure( 88 | NamedLock.acquire(txn2, u"a test lock"), LockTimeout 89 | ) 90 | yield txn2.abort() 91 | self.flushLoggedErrors() 92 | -------------------------------------------------------------------------------- /twext/enterprise/test/test_util.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | import datetime 18 | 19 | from twisted.trial.unittest import TestCase 20 | 21 | from twext.enterprise.util import parseSQLTimestamp 22 | 23 | 24 | class TimestampTests(TestCase): 25 | """ 26 | Tests for date-related functions. 27 | """ 28 | 29 | def test_parseSQLTimestamp(self): 30 | """ 31 | L{parseSQLTimestamp} parses the traditional SQL timestamp. 32 | """ 33 | tests = ( 34 | ("2012-04-04 12:34:56", datetime.datetime(2012, 4, 4, 12, 34, 56)), 35 | ("2012-12-31 01:01:01", datetime.datetime(2012, 12, 31, 1, 1, 1)), 36 | ) 37 | 38 | for sqlStr, result in tests: 39 | self.assertEqual(parseSQLTimestamp(sqlStr), result) 40 | -------------------------------------------------------------------------------- /twext/enterprise/util.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.enterprise.test.test_util -*- 2 | ## 3 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Utilities for dealing with different databases. 20 | """ 21 | 22 | from datetime import datetime 23 | 24 | SQL_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%f" 25 | 26 | 27 | def parseSQLTimestamp(ts): 28 | """ 29 | Parse an SQL timestamp string. 30 | """ 31 | # Handle case where fraction seconds may not be present 32 | if not isinstance(ts, datetime): 33 | if len(ts) < len(SQL_TIMESTAMP_FORMAT): 34 | ts += ".0" 35 | return datetime.strptime(ts, SQL_TIMESTAMP_FORMAT) 36 | else: 37 | return ts 38 | 39 | 40 | def mapOracleOutputType(column): 41 | """ 42 | Map a single output value from cx_Oracle based on some rules and 43 | expectations that we have based on the pgdb bindings. 44 | 45 | @param column: a single value from a column. 46 | 47 | @return: a converted value based on the type of the input; oracle CLOBs and 48 | datetime timestamps will be converted to strings, unicode values will 49 | be converted to UTF-8 encoded byte sequences (C{str}s), and floating 50 | point numbers will be converted to integer types if they are integers. 51 | Any other types will be left alone. 52 | """ 53 | if hasattr(column, "read"): 54 | # Try to detect large objects and format convert them to 55 | # strings on the fly. We need to do this as we read each 56 | # row, due to the issue described here - 57 | # http://cx-oracle.sourceforge.net/html/lob.html - in 58 | # particular, the part where it says "In particular, do not 59 | # use the fetchall() method". 60 | column = column.read() 61 | 62 | elif isinstance(column, float): 63 | # cx_Oracle maps _all_ numbers to float types, which is more 64 | # consistent, but we expect the database to be able to store integers 65 | # as integers (in fact almost all the values in our schema are 66 | # integers), so we map those values which exactly match back into 67 | # integers. 68 | if int(column) == column: 69 | return int(column) 70 | else: 71 | return column 72 | 73 | if isinstance(column, unicode): 74 | # Finally, we process all data as UTF-8 byte strings in order to reduce 75 | # memory consumption. Pass any unicode string values back to the 76 | # application as unicode. 77 | column = column.encode("utf-8") 78 | 79 | return column 80 | -------------------------------------------------------------------------------- /twext/internet/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to L{twisted.internet}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/internet/adaptendpoint.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.internet.test.test_adaptendpoint -*- 2 | ## 3 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Adapter for old-style connectTCP/connectSSL code to use endpoints and be happy; 20 | specifically, to receive the additional duplicate notifications that it wants to 21 | receive, L{clientConnectionLost} and L{clientConnectionFailed} on the factory. 22 | """ 23 | 24 | from zope.interface import implements 25 | 26 | from twisted.internet.interfaces import IConnector 27 | 28 | from twisted.internet.protocol import Factory 29 | from twisted.python import log 30 | 31 | __all__ = [ 32 | "connect" 33 | ] 34 | 35 | 36 | class _WrappedProtocol(object): 37 | """ 38 | A protocol providing a thin wrapper that relays the connectionLost 39 | notification. 40 | """ 41 | 42 | def __init__(self, wrapped, wrapper): 43 | """ 44 | @param wrapped: the wrapped L{IProtocol} provider, to which all methods 45 | will be relayed. 46 | 47 | @param wrapper: The L{LegacyClientFactoryWrapper} that holds the 48 | relevant L{ClientFactory}. 49 | """ 50 | self._wrapped = wrapped 51 | self._wrapper = wrapper 52 | 53 | def __getattr__(self, attr): 54 | """ 55 | Relay all undefined methods to the wrapped protocol. 56 | """ 57 | return getattr(self._wrapped, attr) 58 | 59 | def connectionLost(self, reason): 60 | """ 61 | When the connection is lost, return the connection. 62 | """ 63 | try: 64 | self._wrapped.connectionLost(reason) 65 | except: 66 | log.err() 67 | self._wrapper.legacyFactory.clientConnectionLost(self._wrapper, reason) 68 | 69 | 70 | class LegacyClientFactoryWrapper(Factory): 71 | implements(IConnector) 72 | 73 | def __init__(self, legacyFactory, endpoint): 74 | self.currentlyConnecting = False 75 | self.legacyFactory = legacyFactory 76 | self.endpoint = endpoint 77 | self._connectedProtocol = None 78 | self._outstandingAttempt = None 79 | 80 | def getDestination(self): 81 | """ 82 | Implement L{IConnector.getDestination}. 83 | 84 | @return: the endpoint being connected to as the destination. 85 | """ 86 | return self.endpoint 87 | 88 | def buildProtocol(self, addr): 89 | """ 90 | Implement L{Factory.buildProtocol} to return a wrapper protocol that 91 | will capture C{connectionLost} notifications. 92 | 93 | @return: a L{Protocol}. 94 | """ 95 | return _WrappedProtocol(self.legacyFactory.buildProtocol(addr), self) 96 | 97 | def connect(self): 98 | """ 99 | Implement L{IConnector.connect} to connect the endpoint. 100 | """ 101 | if self._outstandingAttempt is not None: 102 | raise RuntimeError("connection already in progress") 103 | self.legacyFactory.startedConnecting(self) 104 | d = self._outstandingAttempt = self.endpoint.connect(self) 105 | 106 | @d.addBoth 107 | def attemptDone(result): 108 | self._outstandingAttempt = None 109 | return result 110 | 111 | def rememberProto(proto): 112 | self._connectedProtocol = proto 113 | return proto 114 | 115 | def callClientConnectionFailed(reason): 116 | self.legacyFactory.clientConnectionFailed(self, reason) 117 | d.addCallbacks(rememberProto, callClientConnectionFailed) 118 | 119 | def disconnect(self): 120 | """ 121 | Implement L{IConnector.disconnect}. 122 | """ 123 | if self._connectedProtocol is not None: 124 | self._connectedProtocol.transport.loseConnection() 125 | elif self._outstandingAttempt is not None: 126 | self._outstandingAttempt.cancel() 127 | 128 | def stopConnecting(self): 129 | """ 130 | Implement L{IConnector.stopConnecting}. 131 | """ 132 | if self._outstandingAttempt is None: 133 | raise RuntimeError("no connection attempt in progress") 134 | self.disconnect() 135 | 136 | 137 | def connect(endpoint, clientFactory): 138 | """ 139 | Connect a L{twisted.internet.protocol.ClientFactory} to a remote host using 140 | the given L{twisted.internet.interfaces.IStreamClientEndpoint}. This relays 141 | C{clientConnectionFailed} and C{clientConnectionLost} notifications as 142 | legacy code using the L{ClientFactory} interface, such as, 143 | L{ReconnectingClientFactory} would expect. 144 | 145 | @param endpoint: The endpoint to connect to. 146 | @type endpoint: L{twisted.internet.interfaces.IStreamClientEndpoint} 147 | 148 | @param clientFactory: The client factory doing the connecting. 149 | @type clientFactory: L{twisted.internet.protocol.ClientFactory} 150 | 151 | @return: A connector object representing the connection attempt just 152 | initiated. 153 | @rtype: L{IConnector} 154 | """ 155 | wrap = LegacyClientFactoryWrapper(clientFactory, endpoint) 156 | wrap.noisy = clientFactory.noisy # relay the noisy attribute to the wrapper 157 | wrap.connect() 158 | return wrap 159 | -------------------------------------------------------------------------------- /twext/internet/decorate.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Decorators. 19 | """ 20 | 21 | __all__ = [ 22 | "memoizedKey", 23 | ] 24 | 25 | 26 | from inspect import getargspec 27 | 28 | from twisted.internet.defer import Deferred, succeed 29 | 30 | 31 | class Memoizable(object): 32 | """ 33 | A class that stores itself in the memo dictionary. 34 | """ 35 | 36 | def memoMe(self, key, memo): 37 | """ 38 | Add this object to the memo dictionary in whatever fashion is appropriate. 39 | 40 | @param key: key used for lookup 41 | @type key: C{object} (typically C{str} or C{int}) 42 | @param memo: the dict to store to 43 | @type memo: C{dict} 44 | """ 45 | raise NotImplementedError 46 | 47 | 48 | def memoizedKey(keyArgument, memoAttribute, deferredResult=True): 49 | """ 50 | Decorator which memoizes the result of a method on that method's instance. If the instance is derived from 51 | class Memoizable, then the memoMe method is used to store the result, otherwise it is stored directly in 52 | the dict. 53 | 54 | @param keyArgument: The name of the "key" argument. 55 | @type keyArgument: C{str} 56 | 57 | @param memoAttribute: The name of the attribute on the instance which 58 | should be used for memoizing the result of this method; the attribute 59 | itself must be a dictionary. Alternately, if the specified argument is 60 | callable, it is a callable that takes the arguments passed to the 61 | decorated method and returns the memo dictionaries. 62 | @type memoAttribute: C{str} or C{callable} 63 | 64 | @param deferredResult: Whether the result must be a deferred. 65 | """ 66 | def getarg(argname, argspec, args, kw): 67 | """ 68 | Get an argument from some arguments. 69 | 70 | @param argname: The name of the argument to retrieve. 71 | 72 | @param argspec: The result of L{inspect.getargspec}. 73 | 74 | @param args: positional arguments passed to the function specified by 75 | argspec. 76 | 77 | @param kw: keyword arguments passed to the function specified by 78 | argspec. 79 | 80 | @return: The value of the argument named by 'argname'. 81 | """ 82 | argnames = argspec[0] 83 | try: 84 | argpos = argnames.index(argname) 85 | except ValueError: 86 | argpos = None 87 | if argpos is not None: 88 | if len(args) > argpos: 89 | return args[argpos] 90 | if argname in kw: 91 | return kw[argname] 92 | else: 93 | raise TypeError("could not find key argument %r in %r/%r (%r)" % ( 94 | argname, args, kw, argpos 95 | )) 96 | 97 | def decorate(thunk): 98 | # cheater move to try to get the right argspec from inlineCallbacks. 99 | # This could probably be more robust, but the 'cell_contents' thing 100 | # probably can't (that's the only real reference to the underlying 101 | # function). 102 | if thunk.func_code.co_name == "unwindGenerator": 103 | specTarget = thunk.func_closure[0].cell_contents 104 | else: 105 | specTarget = thunk 106 | spec = getargspec(specTarget) 107 | 108 | def outer(*a, **kw): 109 | self = a[0] 110 | if callable(memoAttribute): 111 | memo = memoAttribute(*a, **kw) 112 | else: 113 | memo = getattr(self, memoAttribute) 114 | key = getarg(keyArgument, spec, a, kw) 115 | if key in memo: 116 | memoed = memo[key] 117 | if deferredResult: 118 | return succeed(memoed) 119 | else: 120 | return memoed 121 | result = thunk(*a, **kw) 122 | 123 | if isinstance(result, Deferred): 124 | def memoResult(finalResult): 125 | if isinstance(finalResult, Memoizable): 126 | finalResult.memoMe(key, memo) 127 | elif finalResult is not None: 128 | memo[key] = finalResult 129 | return finalResult 130 | result.addCallback(memoResult) 131 | elif result is not None: 132 | memo[key] = result 133 | return result 134 | 135 | return outer 136 | 137 | return decorate 138 | -------------------------------------------------------------------------------- /twext/internet/fswatch.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Watch the availablity of a file system directory 19 | """ 20 | 21 | import os 22 | from zope.interface import Interface 23 | from twisted.internet import reactor 24 | from twisted.python.log import Logger 25 | 26 | try: 27 | from select import ( 28 | kevent, KQ_FILTER_VNODE, KQ_EV_ADD, KQ_EV_ENABLE, 29 | KQ_EV_CLEAR, KQ_NOTE_DELETE, KQ_NOTE_RENAME, KQ_EV_EOF 30 | ) 31 | kqueueSupported = True 32 | except ImportError: 33 | # kqueue not supported on this platform 34 | kqueueSupported = False 35 | 36 | 37 | class IDirectoryChangeListenee(Interface): 38 | """ 39 | A delegate of DirectoryChangeListener 40 | """ 41 | 42 | def disconnected(): 43 | """ 44 | The directory has been unmounted 45 | """ 46 | 47 | def deleted(): 48 | """ 49 | The directory has been deleted 50 | """ 51 | 52 | def renamed(): 53 | """ 54 | The directory has been renamed 55 | """ 56 | 57 | def connectionLost(reason): 58 | """ 59 | The file descriptor has been closed 60 | """ 61 | 62 | 63 | # TODO: better way to tell if reactor is kqueue or not 64 | if kqueueSupported and hasattr(reactor, "_doWriteOrRead"): 65 | 66 | def patchReactor(reactor): 67 | # Wrap _doWriteOrRead to support KQ_FILTER_VNODE 68 | origDoWriteOrRead = reactor._doWriteOrRead 69 | 70 | def _doWriteOrReadOrVNodeEvent(selectable, fd, event): 71 | origDoWriteOrRead(selectable, fd, event) 72 | if event.filter == KQ_FILTER_VNODE: 73 | selectable.vnodeEventHappened(event) 74 | 75 | reactor._doWriteOrRead = _doWriteOrReadOrVNodeEvent 76 | 77 | patchReactor(reactor) 78 | 79 | class DirectoryChangeListener(Logger, object): 80 | """ 81 | Listens for the removal, renaming, or general unavailability of a 82 | given directory, and lets a delegate listenee know about them. 83 | """ 84 | 85 | def __init__(self, reactor, dirname, listenee): 86 | """ 87 | @param reactor: the reactor 88 | @param dirname: the full path to the directory to watch; it must 89 | already exist 90 | @param listenee: the delegate to call 91 | @type listenee: IDirectoryChangeListenee 92 | """ 93 | self._reactor = reactor 94 | self._fd = os.open(dirname, os.O_RDONLY) 95 | self._dirname = dirname 96 | self._listenee = listenee 97 | 98 | def logPrefix(self): 99 | return repr(self._dirname) 100 | 101 | def fileno(self): 102 | return self._fd 103 | 104 | def vnodeEventHappened(self, evt): 105 | if evt.flags & KQ_EV_EOF: 106 | self._listenee.disconnected() 107 | if evt.fflags & KQ_NOTE_DELETE: 108 | self._listenee.deleted() 109 | if evt.fflags & KQ_NOTE_RENAME: 110 | self._listenee.renamed() 111 | 112 | def startListening(self): 113 | ke = kevent(self._fd, filter=KQ_FILTER_VNODE, 114 | flags=(KQ_EV_ADD | KQ_EV_ENABLE | KQ_EV_CLEAR), 115 | fflags=KQ_NOTE_DELETE | KQ_NOTE_RENAME) 116 | self._reactor._kq.control([ke], 0, None) 117 | self._reactor._selectables[self._fd] = self 118 | 119 | def connectionLost(self, reason): 120 | os.close(self._fd) 121 | self._listenee.connectionLost(reason) 122 | 123 | 124 | else: 125 | 126 | # TODO: implement this for systems without kqueue support: 127 | 128 | class DirectoryChangeListener(Logger, object): 129 | """ 130 | Listens for the removal, renaming, or general unavailability of a 131 | given directory, and lets a delegate listenee know about them. 132 | """ 133 | 134 | def __init__(self, reactor, dirname, listenee): 135 | """ 136 | @param reactor: the reactor 137 | @param dirname: the full path to the directory to watch; it must 138 | already exist 139 | @param listenee: the delegate to call 140 | @type listenee: IDirectoryChangeListenee 141 | """ 142 | self._reactor = reactor 143 | self._fd = os.open(dirname, os.O_RDONLY) 144 | self._dirname = dirname 145 | self._listenee = listenee 146 | 147 | def logPrefix(self): 148 | return repr(self._dirname) 149 | 150 | def fileno(self): 151 | return self._fd 152 | 153 | def vnodeEventHappened(self, evt): 154 | pass 155 | 156 | def startListening(self): 157 | pass 158 | 159 | def connectionLost(self, reason): 160 | os.close(self._fd) 161 | self._listenee.connectionLost(reason) 162 | -------------------------------------------------------------------------------- /twext/internet/gaiendpoint.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.internet.test.test_gaiendpoint -*- 2 | ## 3 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | from __future__ import print_function 18 | 19 | """ 20 | L{getaddrinfo}()-based endpoint 21 | """ 22 | 23 | from socket import getaddrinfo, AF_UNSPEC, AF_INET, AF_INET6, SOCK_STREAM 24 | from twisted.internet.endpoints import TCP4ClientEndpoint, SSL4ClientEndpoint 25 | from twisted.internet.defer import Deferred 26 | from twisted.internet.threads import deferToThread 27 | from twisted.internet.task import LoopingCall 28 | 29 | 30 | class MultiFailure(Exception): 31 | 32 | def __init__(self, failures): 33 | super(MultiFailure, self).__init__("Failure with multiple causes.") 34 | self.failures = failures 35 | 36 | 37 | class GAIEndpoint(object): 38 | """ 39 | Client endpoint that will call L{getaddrinfo} in a thread and then attempt 40 | to connect to each endpoint (almost) in parallel. 41 | 42 | @ivar reactor: The reactor to attempt the connection with. 43 | @type reactor: provider of L{IReactorTCP} and L{IReactorTime} 44 | 45 | @ivar host: The host to resolve. 46 | @type host: L{str} 47 | 48 | @ivar port: The port number to resolve. 49 | @type port: L{int} 50 | 51 | @ivar deferToThread: A function like L{deferToThread}, used to invoke 52 | getaddrinfo. (Replaceable mainly for testing purposes.) 53 | """ 54 | 55 | deferToThread = staticmethod(deferToThread) 56 | 57 | def subEndpoint(self, reactor, host, port, contextFactory): 58 | """ 59 | Create an endpoint to connect to based on a single address result from 60 | L{getaddrinfo}. 61 | 62 | @param reactor: the reactor to connect to 63 | @type reactor: L{IReactorTCP} 64 | 65 | @param host: The IP address of the host to connect to, in presentation 66 | format. 67 | @type host: L{str} 68 | 69 | @param port: The numeric port number to connect to. 70 | @type port: L{int} 71 | 72 | @param contextFactory: If not L{None}, the OpenSSL context factory to 73 | use to produce client connections. 74 | 75 | @return: a stream client endpoint that will connect to the given host 76 | and port via the given reactor. 77 | @rtype: L{IStreamClientEndpoint} 78 | """ 79 | if contextFactory is None: 80 | return TCP4ClientEndpoint(reactor, host, port) 81 | else: 82 | return SSL4ClientEndpoint(reactor, host, port, contextFactory) 83 | 84 | def __init__(self, reactor, host, port, contextFactory=None): 85 | self.reactor = reactor 86 | self.host = host 87 | self.port = port 88 | self.contextFactory = contextFactory 89 | 90 | def connect(self, factory): 91 | dgai = self.deferToThread(getaddrinfo, self.host, self.port, 92 | AF_UNSPEC, SOCK_STREAM) 93 | 94 | @dgai.addCallback 95 | def gaiToEndpoints(gairesult): 96 | for family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in gairesult: 97 | if family in [AF_INET6, AF_INET]: 98 | yield self.subEndpoint(self.reactor, sockaddr[0], 99 | sockaddr[1], self.contextFactory) 100 | 101 | @gaiToEndpoints.addCallback 102 | def connectTheEndpoints(endpoints): 103 | doneTrying = [] 104 | outstanding = [] 105 | errors = [] 106 | succeeded = [] 107 | actuallyDidIt = Deferred() 108 | 109 | def removeMe(result, attempt): 110 | outstanding.remove(attempt) 111 | return result 112 | 113 | def connectingDone(result): 114 | if lc.running: 115 | lc.stop() 116 | succeeded.append(True) 117 | for o in outstanding[::]: 118 | o.cancel() 119 | actuallyDidIt.callback(result) 120 | return None 121 | 122 | def lastChance(): 123 | if doneTrying and not outstanding and not succeeded: 124 | # We've issued our last attempts. There are no remaining 125 | # outstanding attempts; they've all failed. We haven't 126 | # succeeded. Time... to die. 127 | actuallyDidIt.errback(MultiFailure(errors)) 128 | 129 | def connectingFailed(why): 130 | errors.append(why) 131 | lastChance() 132 | return None 133 | 134 | def nextOne(): 135 | try: 136 | endpoint = endpoints.next() 137 | except StopIteration: 138 | # Out of endpoints to try! Now it's time to wait for all of 139 | # the outstanding attempts to complete, and, if none of them 140 | # have been successful, then to give up with a relevant 141 | # error. They'll all be dealt with by connectingDone or 142 | # connectingFailed. 143 | doneTrying.append(True) 144 | lc.stop() 145 | lastChance() 146 | else: 147 | attempt = endpoint.connect(factory) 148 | attempt.addBoth(removeMe, attempt) 149 | attempt.addCallbacks(connectingDone, connectingFailed) 150 | outstanding.append(attempt) 151 | lc = LoopingCall(nextOne) 152 | lc.clock = self.reactor 153 | lc.start(0.0) 154 | return actuallyDidIt 155 | return dgai 156 | 157 | 158 | if __name__ == '__main__': 159 | from twisted.internet import reactor 160 | import sys 161 | if sys.argv[1:]: 162 | host = sys.argv[1] 163 | port = int(sys.argv[2]) 164 | else: 165 | host = "localhost" 166 | port = 22 167 | gaie = GAIEndpoint(reactor, host, port) 168 | 169 | from twisted.internet.protocol import Factory, Protocol 170 | 171 | class HelloGoobye(Protocol, object): 172 | 173 | def connectionMade(self): 174 | print('Hello!') 175 | self.transport.loseConnection() 176 | 177 | def connectionLost(self, reason): 178 | print('Goodbye') 179 | 180 | class MyFactory(Factory, object): 181 | 182 | def buildProtocol(self, addr): 183 | print('Building protocol for:', addr) 184 | return HelloGoobye() 185 | 186 | def bye(what): 187 | print('bye', what) 188 | reactor.stop() 189 | 190 | gaie.connect(MyFactory()).addBoth(bye) 191 | reactor.run() 192 | -------------------------------------------------------------------------------- /twext/internet/socketfile.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2015-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Socket file implementation for MaxAccept 19 | """ 20 | 21 | __all__ = [ 22 | "MaxAcceptSocketFileServer", 23 | ] 24 | 25 | 26 | from twisted.application import service 27 | from twisted.internet import endpoints 28 | from twisted.internet.defer import inlineCallbacks 29 | 30 | from twext.python.log import Logger 31 | 32 | log = Logger() 33 | 34 | 35 | def maxAcceptDoRead(self): 36 | self.numberAccepts = min( 37 | self.factory.maxRequests - self.factory.outstandingRequests, 38 | self.factory.maxAccepts 39 | ) 40 | self.realDoRead() 41 | 42 | 43 | class MaxAcceptSocketFileServer(service.Service): 44 | """ 45 | Socket File server 46 | 47 | @ivar myPort: When running, this is set to the L{IListeningPort} being 48 | managed by this service. 49 | """ 50 | 51 | def __init__(self, protocolFactory, address, backlog=None): 52 | self.protocolFactory = protocolFactory 53 | self.protocolFactory.myServer = self 54 | self.address = address 55 | self.backlog = backlog 56 | self.myPort = None 57 | 58 | @inlineCallbacks 59 | def startService(self): 60 | from twisted.internet import reactor 61 | endpoint = endpoints.UNIXServerEndpoint( 62 | reactor, self.address, backlog=self.backlog, wantPID=1 63 | ) 64 | self.myPort = yield endpoint.listen(self.protocolFactory) 65 | 66 | # intercept doRead() to set numberAccepts 67 | self.myPort.realDoRead = self.myPort.doRead 68 | self.myPort.doRead = maxAcceptDoRead.__get__( 69 | self.myPort, self.myPort.__class__ 70 | ) 71 | 72 | @inlineCallbacks 73 | def stopService(self): 74 | """ 75 | Wait for outstanding requests to finish 76 | 77 | @return: a Deferred which fires when all outstanding requests are 78 | complete 79 | """ 80 | if self.myPort is not None: 81 | yield self.myPort.stopListening() 82 | 83 | if hasattr(self.protocolFactory, "allConnectionsClosed"): 84 | yield self.protocolFactory.allConnectionsClosed() 85 | -------------------------------------------------------------------------------- /twext/internet/spawnsvc.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2011-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Utility service that can spawn subprocesses. 19 | """ 20 | 21 | import os 22 | import sys 23 | 24 | from twisted.python import log 25 | 26 | from twisted.python.reflect import namedAny 27 | from twisted.internet.stdio import StandardIO 28 | from twisted.internet.error import ReactorNotRunning 29 | 30 | if __name__ == '__main__': 31 | 32 | sys.stdout = sys.stderr 33 | there = sys.argv[1] 34 | protocolClass = namedAny(there) 35 | proto = protocolClass() 36 | origLost = proto.connectionLost 37 | 38 | def goodbye(reason): 39 | """ 40 | Stop the process if stdin is closed. 41 | """ 42 | try: 43 | reactor.stop() 44 | except ReactorNotRunning: 45 | pass 46 | return origLost(reason) 47 | proto.connectionLost = goodbye 48 | StandardIO(proto) 49 | from twisted.internet import reactor 50 | reactor.run() 51 | os._exit(0) 52 | 53 | 54 | from zope.interface import implements 55 | 56 | from twisted.internet.interfaces import ITransport, IPushProducer, IConsumer 57 | 58 | from twisted.application.service import Service 59 | from twisted.python.reflect import qual 60 | from twisted.internet.protocol import ProcessProtocol 61 | from twisted.internet.defer import Deferred, succeed 62 | 63 | 64 | class BridgeTransport(object): 65 | """ 66 | ITransport implementation for the protocol in the parent process running a 67 | L{SpawnerService}. 68 | """ 69 | 70 | implements(ITransport, IPushProducer, IConsumer) 71 | 72 | def __init__(self, processTransport): 73 | """ 74 | Create this bridge transport connected to an L{IProcessTransport}. 75 | """ 76 | self.transport = processTransport 77 | 78 | def __getattr__(self, name): 79 | """ 80 | Delegate all attribute accesses to the process traansport. 81 | """ 82 | return getattr(self.transport, name) 83 | 84 | def getPeer(self): 85 | """ 86 | Get a fake peer address indicating the subprocess's pid. 87 | """ 88 | return "Peer:PID:" + str(self.transport.pid) 89 | 90 | def getHost(self): 91 | """ 92 | Get a fake host address indicating the subprocess's pid. 93 | """ 94 | return "Host:PID:" + str(self.transport.pid) 95 | 96 | 97 | class BridgeProtocol(ProcessProtocol, object): 98 | """ 99 | Process protocol implementation that delivers data to the C{hereProto} 100 | associated with an invocation of L{SpawnerService.spawn}. 101 | 102 | @ivar service: a L{SpawnerService} that created this L{BridgeProtocol} 103 | 104 | @ivar protocol: a reference to the L{IProtocol}. 105 | 106 | @ivar killTimeout: number of seconds after sending SIGINT that this process 107 | will send SIGKILL. 108 | """ 109 | 110 | def __init__(self, service, protocol, killTimeout=15.0): 111 | self.service = service 112 | self.protocol = protocol 113 | self.killTimeout = killTimeout 114 | self.service.addBridge(self) 115 | 116 | def connectionMade(self): 117 | """ 118 | The subprocess was started. 119 | """ 120 | self.protocol.makeConnection(BridgeTransport(self.transport)) 121 | 122 | def outReceived(self, data): 123 | """ 124 | Some data was received to standard output; relay it to the protocol. 125 | """ 126 | self.protocol.dataReceived(data) 127 | 128 | def errReceived(self, data): 129 | """ 130 | Some standard error was received from the subprocess. 131 | """ 132 | log.msg("Error output from process: " + data, 133 | isError=True) 134 | 135 | _killTimeout = None 136 | 137 | def eventuallyStop(self): 138 | """ 139 | Eventually stop this subprocess. Send it a SIGTERM, and if it hasn't 140 | stopped by C{self.killTimeout} seconds, send it a SIGKILL. 141 | """ 142 | self.transport.signalProcess('TERM') 143 | 144 | def reallyStop(): 145 | self.transport.signalProcess("KILL") 146 | self._killTimeout = None 147 | self._killTimeout = ( 148 | self.service.reactor.callLater(self.killTimeout, reallyStop) 149 | ) 150 | 151 | def processEnded(self, reason): 152 | """ 153 | The process has ended; notify the L{SpawnerService} that this bridge 154 | has stopped. 155 | """ 156 | if self._killTimeout is not None: 157 | self._killTimeout.cancel() 158 | self.protocol.connectionLost(reason) 159 | self.service.removeBridge(self) 160 | 161 | 162 | class SpawnerService(Service, object): 163 | """ 164 | Process to spawn services and then shut them down. 165 | 166 | @ivar reactor: an L{IReactorProcess}/L{IReactorTime} 167 | 168 | @ivar pendingSpawns: a C{list} of 2-C{tuple}s of hereProto, thereProto. 169 | 170 | @ivar bridges: a C{list} of L{BridgeProtocol} instances. 171 | """ 172 | 173 | def __init__(self, reactor=None): 174 | if reactor is None: 175 | from twisted.internet import reactor 176 | self.reactor = reactor 177 | self.pendingSpawns = [] 178 | self.bridges = [] 179 | self._stopAllDeferred = None 180 | 181 | def spawn(self, hereProto, thereProto, childFDs=None): 182 | """ 183 | Spawn a subprocess with a connected pair of protocol objects, one in 184 | the current process, one in the subprocess. 185 | 186 | @param hereProto: a L{Protocol} instance to listen in this process. 187 | 188 | @param thereProto: a top-level class or function that will be imported 189 | and called in the spawned subprocess. 190 | 191 | @param childFDs: File descriptors to share with the subprocess; same 192 | format as L{IReactorProcess.spawnProcess}. 193 | 194 | @return: a L{Deferred} that fires when C{hereProto} is ready. 195 | """ 196 | if not self.running: 197 | self.pendingSpawns.append((hereProto, thereProto)) 198 | return 199 | name = qual(thereProto) 200 | argv = [sys.executable, '-u', '-m', __name__, name] 201 | self.reactor.spawnProcess( 202 | BridgeProtocol(self, hereProto), sys.executable, 203 | argv, os.environ, childFDs=childFDs 204 | ) 205 | return succeed(hereProto) 206 | 207 | def startService(self): 208 | """ 209 | Start the service; spawn any processes previously started with spawn(). 210 | """ 211 | super(SpawnerService, self).startService() 212 | for spawn in self.pendingSpawns: 213 | self.spawn(*spawn) 214 | self.pendingSpawns = [] 215 | 216 | def addBridge(self, bridge): 217 | """ 218 | Add a L{BridgeProtocol} to the list to be tracked. 219 | """ 220 | self.bridges.append(bridge) 221 | 222 | def removeBridge(self, bridge): 223 | """ 224 | The process controlled by a L{BridgeProtocol} has terminated; remove it 225 | from the active list, and fire any outstanding Deferred. 226 | 227 | @param bridge: the protocol which has ended. 228 | """ 229 | self.bridges.remove(bridge) 230 | if self._stopAllDeferred is not None: 231 | if len(self.bridges) == 0: 232 | self._stopAllDeferred.callback(None) 233 | self._stopAllDeferred = None 234 | 235 | def stopService(self): 236 | """ 237 | Stop the service. 238 | """ 239 | super(SpawnerService, self).stopService() 240 | if self.bridges: 241 | self._stopAllDeferred = Deferred() 242 | for bridge in self.bridges: 243 | bridge.eventuallyStop() 244 | return self._stopAllDeferred 245 | return succeed(None) 246 | -------------------------------------------------------------------------------- /twext/internet/ssl.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to twisted.internet.ssl. 19 | """ 20 | 21 | __all__ = [ 22 | "ChainingOpenSSLContextFactory", 23 | "simpleClientContextFactory", 24 | ] 25 | 26 | import OpenSSL 27 | from OpenSSL.SSL import Context as SSLContext, SSLv23_METHOD, OP_NO_SSLv2, \ 28 | OP_CIPHER_SERVER_PREFERENCE, OP_NO_SSLv3, VERIFY_NONE, VERIFY_PEER, \ 29 | VERIFY_FAIL_IF_NO_PEER_CERT, VERIFY_CLIENT_ONCE 30 | 31 | from twisted.internet.ssl import DefaultOpenSSLContextFactory 32 | from twisted.internet._sslverify import Certificate, _tolerateErrors, VerificationError, verifyHostname 33 | from twisted.python.failure import Failure 34 | 35 | import uuid 36 | 37 | 38 | _OP_NO_COMPRESSION = getattr(OpenSSL.SSL, 'OP_NO_COMPRESSION', 0x00020000) 39 | SSL_CB_HANDSHAKE_DONE = 0x20 40 | 41 | 42 | class ChainingOpenSSLContextFactory (DefaultOpenSSLContextFactory): 43 | 44 | def __init__( 45 | self, privateKeyFileName, certificateFileName, 46 | sslmethod=SSLv23_METHOD, 47 | certificateChainFile=None, keychainIdentity=None, 48 | passwdCallback=None, ciphers=None, 49 | verifyClient=False, requireClientCertificate=False, 50 | verifyClientOnce=True, verifyClientDepth=9, 51 | clientCACertFileNames=[], sendCAsToClient=True, 52 | peerName=None 53 | ): 54 | self.certificateChainFile = certificateChainFile 55 | self.keychainIdentity = keychainIdentity 56 | self.passwdCallback = passwdCallback 57 | self.ciphers = ciphers 58 | 59 | self.peerName = peerName 60 | 61 | self.verifyClient = verifyClient 62 | self.requireClientCertificate = requireClientCertificate 63 | self.verifyClientOnce = verifyClientOnce 64 | self.verifyClientDepth = verifyClientDepth 65 | self.clientCACertFileNames = clientCACertFileNames 66 | self.sendCAsToClient = sendCAsToClient 67 | 68 | DefaultOpenSSLContextFactory.__init__( 69 | self, 70 | privateKeyFileName, 71 | certificateFileName, 72 | sslmethod=sslmethod 73 | ) 74 | 75 | def cacheContext(self): 76 | # Unfortunate code duplication. 77 | ctx = SSLContext(self.sslmethod) 78 | 79 | # Always disable SSLv2/SSLv3/Compression 80 | ctx.set_options(OP_NO_SSLv2) 81 | ctx.set_options(OP_NO_SSLv3) 82 | ctx.set_options(_OP_NO_COMPRESSION) 83 | 84 | if self.ciphers is not None: 85 | ctx.set_cipher_list(self.ciphers) 86 | ctx.set_options(OP_CIPHER_SERVER_PREFERENCE) 87 | 88 | if self.passwdCallback is not None: 89 | ctx.set_passwd_cb(self.passwdCallback) 90 | 91 | if self.keychainIdentity and hasattr(ctx, "use_keychain_identity"): 92 | ctx.use_keychain_identity(self.keychainIdentity) 93 | else: 94 | if self.certificateFileName: 95 | ctx.use_certificate_file(self.certificateFileName) 96 | if self.privateKeyFileName: 97 | ctx.use_privatekey_file(self.privateKeyFileName) 98 | if self.certificateChainFile: 99 | ctx.use_certificate_chain_file(self.certificateChainFile) 100 | 101 | verifyFlags = VERIFY_NONE 102 | if self.verifyClient: 103 | verifyFlags = VERIFY_PEER 104 | if self.requireClientCertificate: 105 | verifyFlags |= VERIFY_FAIL_IF_NO_PEER_CERT 106 | if self.verifyClientOnce: 107 | verifyFlags |= VERIFY_CLIENT_ONCE 108 | if self.clientCACertFileNames: 109 | store = ctx.get_cert_store() 110 | for cert in self.clientCACertFileNames: 111 | with open(cert) as f: 112 | certpem = f.read() 113 | cert = Certificate.loadPEM(certpem) 114 | store.add_cert(cert.original) 115 | if self.sendCAsToClient: 116 | ctx.add_client_ca(cert.original) 117 | 118 | # When a client certificate is used we also need to set a session context id 119 | # to avoid openssl SSL_F_SSL_GET_PREV_SESSION,SSL_R_SESSION_ID_CONTEXT_UNINITIALIZED 120 | # errors 121 | ctx.set_session_id(str(uuid.uuid4()).replace("-", "")) 122 | 123 | # It'd be nice if pyOpenSSL let us pass None here for this behavior (as 124 | # the underlying OpenSSL API call allows NULL to be passed). It 125 | # doesn't, so we'll supply a function which does the same thing. 126 | def _verifyCallback(conn, cert, errno, depth, preverify_ok): 127 | return preverify_ok 128 | ctx.set_verify(verifyFlags, _verifyCallback) 129 | 130 | if self.verifyClientDepth is not None: 131 | ctx.set_verify_depth(self.verifyClientDepth) 132 | 133 | if self.peerName: 134 | if hasattr(ctx, "set_peer_name"): 135 | ctx.set_peer_name(self.peerName) 136 | elif hasattr(ctx, "set_info_callback"): 137 | ctx.set_info_callback( 138 | _tolerateErrors(self._identityVerifyingInfoCallback) 139 | ) 140 | else: 141 | raise ValueError("No suitable SSL API for verifying the peer host name") 142 | 143 | self._context = ctx 144 | 145 | def _identityVerifyingInfoCallback(self, connection, where, ret): 146 | """ 147 | U{info_callback 148 | 149 | } for pyOpenSSL that verifies the hostname in the presented certificate 150 | matches the one passed to this L{ClientTLSOptions}. 151 | 152 | @param connection: the connection which is handshaking. 153 | @type connection: L{OpenSSL.SSL.Connection} 154 | 155 | @param where: flags indicating progress through a TLS handshake. 156 | @type where: L{int} 157 | 158 | @param ret: ignored 159 | @type ret: ignored 160 | """ 161 | if where & SSL_CB_HANDSHAKE_DONE: 162 | try: 163 | hostname = self.peerName.decode("utf-8") if isinstance(self.peerName, str) else self.peerName 164 | verifyHostname(connection, hostname) 165 | except VerificationError: 166 | f = Failure() 167 | transport = connection.get_app_data() 168 | transport.failVerification(f) 169 | 170 | 171 | def simpleClientContextFactory(hostname): 172 | """ 173 | Get a client context factory. 174 | """ 175 | return ChainingOpenSSLContextFactory( 176 | "", "", 177 | certificateChainFile="", 178 | keychainIdentity="", 179 | peerName=hostname, 180 | ) 181 | -------------------------------------------------------------------------------- /twext/internet/tcp.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extentions to twisted.internet.tcp. 19 | """ 20 | 21 | __all__ = [ 22 | "MaxAcceptTCPServer", 23 | "MaxAcceptSSLServer", 24 | ] 25 | 26 | import socket 27 | from OpenSSL import SSL 28 | 29 | from twisted.application import internet 30 | from twisted.internet import tcp, ssl 31 | from twisted.internet.defer import succeed 32 | 33 | from twext.python.log import Logger 34 | 35 | log = Logger() 36 | 37 | 38 | class MaxAcceptPortMixin(object): 39 | """ 40 | Mixin for resetting maxAccepts. 41 | """ 42 | 43 | def doRead(self): 44 | self.numberAccepts = min( 45 | self.factory.maxRequests - self.factory.outstandingRequests, 46 | self.factory.maxAccepts 47 | ) 48 | tcp.Port.doRead(self) 49 | 50 | 51 | class MaxAcceptTCPPort(MaxAcceptPortMixin, tcp.Port): 52 | """ 53 | Use for non-inheriting tcp ports. 54 | """ 55 | 56 | 57 | class MaxAcceptSSLPort(MaxAcceptPortMixin, ssl.Port): 58 | """ 59 | Use for non-inheriting SSL ports. 60 | """ 61 | 62 | 63 | class InheritedTCPPort(MaxAcceptTCPPort): 64 | """ 65 | A tcp port which uses an inherited file descriptor. 66 | """ 67 | 68 | def __init__(self, fd, factory, reactor): 69 | tcp.Port.__init__(self, 0, factory, reactor=reactor) 70 | # MOR: careful because fromfd dup()'s the socket, so we need to 71 | # make sure we don't leak file descriptors 72 | self.socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) 73 | self._realPortNumber = self.port = self.socket.getsockname()[1] 74 | 75 | def createInternetSocket(self): 76 | return self.socket 77 | 78 | def startListening(self): 79 | log.info( 80 | "{self.factory.__class__} starting on {self._realPortNumber}", 81 | self=self 82 | ) 83 | self.factory.doStart() 84 | self.connected = 1 85 | self.fileno = self.socket.fileno 86 | self.numberAccepts = self.factory.maxRequests 87 | self.startReading() 88 | 89 | 90 | class InheritedSSLPort(InheritedTCPPort): 91 | """ 92 | An SSL port which uses an inherited file descriptor. 93 | """ 94 | 95 | _socketShutdownMethod = 'sock_shutdown' 96 | 97 | transport = ssl.Server 98 | 99 | def __init__(self, fd, factory, ctxFactory, reactor): 100 | InheritedTCPPort.__init__(self, fd, factory, reactor) 101 | self.ctxFactory = ctxFactory 102 | self.socket = SSL.Connection(self.ctxFactory.getContext(), self.socket) 103 | 104 | def _preMakeConnection(self, transport): 105 | transport._startTLS() 106 | return tcp.Port._preMakeConnection(self, transport) 107 | 108 | 109 | def _allConnectionsClosed(protocolFactory): 110 | """ 111 | Check to see if protocolFactory implements allConnectionsClosed( ) and 112 | if so, call it. Otherwise, return immediately. 113 | This allows graceful shutdown by waiting for all requests to be completed. 114 | 115 | @param protocolFactory: (usually) an HTTPFactory implementing 116 | allConnectionsClosed which returns a Deferred which fires when all 117 | connections are closed. 118 | 119 | @return: A Deferred firing None when all connections are closed, or 120 | immediately if the given factory does not track its connections (e.g. 121 | InheritingProtocolFactory) 122 | """ 123 | if hasattr(protocolFactory, "allConnectionsClosed"): 124 | return protocolFactory.allConnectionsClosed() 125 | return succeed(None) 126 | 127 | 128 | class MaxAcceptTCPServer(internet.TCPServer): 129 | """ 130 | TCP server which will uses MaxAcceptTCPPorts (and optionally, 131 | inherited ports) 132 | 133 | @ivar myPort: When running, this is set to the L{IListeningPort} being 134 | managed by this service. 135 | """ 136 | 137 | def __init__(self, *args, **kwargs): 138 | internet.TCPServer.__init__(self, *args, **kwargs) 139 | self.protocolFactory = self.args[1] 140 | self.protocolFactory.myServer = self 141 | self.inherit = self.kwargs.get("inherit", False) 142 | self.backlog = self.kwargs.get("backlog", None) 143 | self.interface = self.kwargs.get("interface", None) 144 | 145 | def _getPort(self): 146 | from twisted.internet import reactor 147 | 148 | if self.inherit: 149 | port = InheritedTCPPort(self.args[0], self.args[1], reactor) 150 | else: 151 | port = MaxAcceptTCPPort( 152 | self.args[0], self.args[1], 153 | self.backlog, self.interface, reactor 154 | ) 155 | 156 | port.startListening() 157 | self.myPort = port 158 | return port 159 | 160 | def stopService(self): 161 | """ 162 | Wait for outstanding requests to finish 163 | 164 | @return: a Deferred which fires when all outstanding requests are 165 | complete 166 | """ 167 | internet.TCPServer.stopService(self) 168 | return _allConnectionsClosed(self.protocolFactory) 169 | 170 | 171 | class MaxAcceptSSLServer(internet.SSLServer): 172 | """ 173 | SSL server which will uses MaxAcceptSSLPorts (and optionally, 174 | inherited ports) 175 | """ 176 | 177 | def __init__(self, *args, **kwargs): 178 | internet.SSLServer.__init__(self, *args, **kwargs) 179 | self.protocolFactory = self.args[1] 180 | self.protocolFactory.myServer = self 181 | self.inherit = self.kwargs.get("inherit", False) 182 | self.backlog = self.kwargs.get("backlog", None) 183 | self.interface = self.kwargs.get("interface", None) 184 | 185 | def _getPort(self): 186 | from twisted.internet import reactor 187 | 188 | if self.inherit: 189 | port = InheritedSSLPort( 190 | self.args[0], self.args[1], self.args[2], reactor 191 | ) 192 | else: 193 | port = MaxAcceptSSLPort( 194 | self.args[0], self.args[1], self.args[2], 195 | self.backlog, self.interface, self.reactor 196 | ) 197 | 198 | port.startListening() 199 | self.myPort = port 200 | return port 201 | 202 | def stopService(self): 203 | """ 204 | Wait for outstanding requests to finish 205 | 206 | @return: a Deferred which fires when all outstanding requests are 207 | complete. 208 | """ 209 | internet.SSLServer.stopService(self) 210 | # TODO: check for an ICompletionWaiter interface 211 | return _allConnectionsClosed(self.protocolFactory) 212 | -------------------------------------------------------------------------------- /twext/internet/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.internet}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/internet/test/test_fswatch.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.internet.fswatch}. 19 | """ 20 | 21 | from twext.internet.fswatch import ( 22 | DirectoryChangeListener, patchReactor, IDirectoryChangeListenee 23 | ) 24 | from twisted.internet.kqreactor import KQueueReactor 25 | from twisted.python.filepath import FilePath 26 | from twisted.trial.unittest import TestCase 27 | from zope.interface import implements 28 | 29 | 30 | class KQueueReactorTestFixture(object): 31 | 32 | def __init__(self, testCase, action=None, timeout=10): 33 | """ 34 | Creates a kqueue reactor for use in unit tests. The reactor is patched 35 | with the vnode event handler. Once the reactor is running, it will 36 | call a supplied method. It's expected that the method will ultimately 37 | trigger the stop() of the reactor. The reactor will time out after 10 38 | seconds. 39 | 40 | @param testCase: a test method which is needed for adding cleanup to 41 | @param action: a method which will get called after the reactor is 42 | running 43 | @param timeout: how many seconds to keep the reactor running before 44 | giving up and stopping it 45 | """ 46 | self.testCase = testCase 47 | self.reactor = KQueueReactor() 48 | patchReactor(self.reactor) 49 | self.action = action 50 | self.timeout = timeout 51 | 52 | def maybeStop(): 53 | if self.reactor.running: 54 | return self.reactor.stop() 55 | 56 | self.testCase.addCleanup(maybeStop) 57 | 58 | def runReactor(self): 59 | """ 60 | Run the test reactor, adding cleanup code to stop if after a timeout, 61 | and calling the action method 62 | """ 63 | def getReadyToStop(): 64 | self.reactor.callLater(self.timeout, self.reactor.stop) 65 | self.reactor.callWhenRunning(getReadyToStop) 66 | if self.action is not None: 67 | self.reactor.callWhenRunning(self.action) 68 | self.reactor.run(installSignalHandlers=False) 69 | 70 | 71 | class DataStoreMonitor(object): 72 | """ 73 | Stub IDirectoryChangeListenee 74 | """ 75 | implements(IDirectoryChangeListenee) 76 | 77 | def __init__(self, reactor, storageService): 78 | """ 79 | @param storageService: the service making use of the DataStore 80 | directory; we send it a hardStop() to shut it down 81 | """ 82 | self._reactor = reactor 83 | self._storageService = storageService 84 | self.methodCalled = "" 85 | 86 | def disconnected(self): 87 | self.methodCalled = "disconnected" 88 | self._storageService.hardStop() 89 | self._reactor.stop() 90 | 91 | def deleted(self): 92 | self.methodCalled = "deleted" 93 | self._storageService.hardStop() 94 | self._reactor.stop() 95 | 96 | def renamed(self): 97 | self.methodCalled = "renamed" 98 | self._storageService.hardStop() 99 | self._reactor.stop() 100 | 101 | def connectionLost(self, reason): 102 | pass 103 | 104 | 105 | class StubStorageService(object): 106 | """ 107 | Implements hardStop for testing 108 | """ 109 | 110 | def __init__(self, ignored): 111 | self.stopCalled = False 112 | 113 | def hardStop(self): 114 | self.stopCalled = True 115 | 116 | 117 | class DirectoryChangeListenerTestCase(TestCase): 118 | 119 | def test_delete(self): 120 | """ 121 | Verify directory deletions can be monitored 122 | """ 123 | 124 | self.tmpdir = FilePath(self.mktemp()) 125 | self.tmpdir.makedirs() 126 | 127 | def deleteAction(): 128 | self.tmpdir.remove() 129 | 130 | resource = KQueueReactorTestFixture(self, deleteAction) 131 | storageService = StubStorageService(resource.reactor) 132 | delegate = DataStoreMonitor(resource.reactor, storageService) 133 | dcl = DirectoryChangeListener(resource.reactor, self.tmpdir.path, delegate) 134 | dcl.startListening() 135 | resource.runReactor() 136 | self.assertTrue(storageService.stopCalled) 137 | self.assertEquals(delegate.methodCalled, "deleted") 138 | 139 | def test_rename(self): 140 | """ 141 | Verify directory renames can be monitored 142 | """ 143 | 144 | self.tmpdir = FilePath(self.mktemp()) 145 | self.tmpdir.makedirs() 146 | 147 | def renameAction(): 148 | self.tmpdir.moveTo(FilePath(self.mktemp())) 149 | 150 | resource = KQueueReactorTestFixture(self, renameAction) 151 | storageService = StubStorageService(resource.reactor) 152 | delegate = DataStoreMonitor(resource.reactor, storageService) 153 | dcl = DirectoryChangeListener(resource.reactor, self.tmpdir.path, delegate) 154 | dcl.startListening() 155 | resource.runReactor() 156 | self.assertTrue(storageService.stopCalled) 157 | self.assertEquals(delegate.methodCalled, "renamed") 158 | -------------------------------------------------------------------------------- /twext/internet/test/test_gaiendpoint.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Test cases for L{twext.internet.gaiendpoint} 19 | """ 20 | 21 | from socket import getaddrinfo, AF_INET, SOCK_STREAM 22 | 23 | from twext.internet.gaiendpoint import GAIEndpoint 24 | from twisted.trial.unittest import TestCase 25 | from twisted.internet.defer import Deferred 26 | from twisted.internet.protocol import Factory, Protocol 27 | from twisted.internet.task import Clock 28 | 29 | 30 | class FakeTCPEndpoint(object): 31 | 32 | def __init__(self, reactor, host, port, contextFactory): 33 | self._reactor = reactor 34 | self._host = host 35 | self._port = port 36 | self._attempt = None 37 | self._contextFactory = contextFactory 38 | 39 | def connect(self, factory): 40 | self._attempt = Deferred() 41 | self._factory = factory 42 | return self._attempt 43 | 44 | 45 | class GAIEndpointTestCase(TestCase): 46 | """ 47 | Test cases for L{GAIEndpoint}. 48 | """ 49 | 50 | def makeEndpoint(self, host="abcd.example.com", port=4321): 51 | gaie = GAIEndpoint(self.clock, host, port) 52 | gaie.subEndpoint = self.subEndpoint 53 | gaie.deferToThread = self.deferToSomething 54 | return gaie 55 | 56 | def subEndpoint(self, reactor, host, port, contextFactory): 57 | ftcpe = FakeTCPEndpoint(reactor, host, port, contextFactory) 58 | self.fakeRealEndpoints.append(ftcpe) 59 | return ftcpe 60 | 61 | def deferToSomething(self, func, *a, **k): 62 | """ 63 | Test replacement for L{deferToThread}, which can only call 64 | L{getaddrinfo}. 65 | """ 66 | d = Deferred() 67 | if func is not getaddrinfo: 68 | self.fail("Only getaddrinfo should be invoked in a thread.") 69 | self.inThreads.append((d, func, a, k)) 70 | return d 71 | 72 | def gaiResult(self, family, socktype, proto, canonname, sockaddr): 73 | """ 74 | A call to L{getaddrinfo} has succeeded; invoke the L{Deferred} waiting 75 | on it. 76 | """ 77 | d, _ignore_f, _ignore_a, _ignore_k = self.inThreads.pop(0) 78 | d.callback([(family, socktype, proto, canonname, sockaddr)]) 79 | 80 | def setUp(self): 81 | """ 82 | Set up! 83 | """ 84 | self.inThreads = [] 85 | self.clock = Clock() 86 | self.fakeRealEndpoints = [] 87 | self.makeEndpoint() 88 | 89 | def test_simpleSuccess(self): 90 | """ 91 | If C{getaddrinfo} gives one L{GAIEndpoint.connect}. 92 | """ 93 | gaiendpoint = self.makeEndpoint() 94 | protos = [] 95 | f = Factory() 96 | f.protocol = Protocol 97 | gaiendpoint.connect(f).addCallback(protos.append) 98 | WHO_CARES = 0 99 | WHAT_EVER = "" 100 | self.gaiResult(AF_INET, SOCK_STREAM, WHO_CARES, WHAT_EVER, 101 | ("1.2.3.4", 4321)) 102 | self.clock.advance(1.0) 103 | attempt = self.fakeRealEndpoints[0]._attempt 104 | attempt.callback(self.fakeRealEndpoints[0]._factory.buildProtocol(None)) 105 | self.assertEqual(len(protos), 1) 106 | -------------------------------------------------------------------------------- /twext/internet/threadutils.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | import sys 18 | from Queue import Queue 19 | 20 | 21 | from twisted.python.failure import Failure 22 | from twisted.internet.defer import Deferred 23 | 24 | 25 | _DONE = object() 26 | 27 | _STATE_STARTING = 'STARTING' 28 | _STATE_RUNNING = 'RUNNING' 29 | _STATE_STOPPING = 'STOPPING' 30 | _STATE_STOPPED = 'STOPPED' 31 | 32 | 33 | class ThreadHolder(object): 34 | """ 35 | A queue which will hold a reactor threadpool thread open until all of the 36 | work in that queue is done. 37 | """ 38 | 39 | def __init__(self, reactor): 40 | self._reactor = reactor 41 | self._state = _STATE_STOPPED 42 | self._stopper = None 43 | self._q = None 44 | self._retryCallback = None 45 | 46 | def _run(self): 47 | """ 48 | Worker function which runs in a non-reactor thread. 49 | """ 50 | self._state = _STATE_RUNNING 51 | while self._qpull(): 52 | pass 53 | 54 | def _qpull(self): 55 | """ 56 | Pull one item off the queue and react appropriately. 57 | 58 | Return whether or not to keep going. 59 | """ 60 | work = self._q.get() 61 | if work is _DONE: 62 | def finishStopping(): 63 | self._state = _STATE_STOPPED 64 | self._q = None 65 | s = self._stopper 66 | self._stopper = None 67 | s.callback(None) 68 | self._reactor.callFromThread(finishStopping) 69 | return False 70 | self._oneWorkUnit(*work) 71 | return True 72 | 73 | def _oneWorkUnit(self, deferred, instruction): 74 | try: 75 | result = instruction() 76 | except: 77 | etype, evalue, etb = sys.exc_info() 78 | 79 | def relayFailure(): 80 | f = Failure(evalue, etype, etb) 81 | deferred.errback(f) 82 | self._reactor.callFromThread(relayFailure) 83 | else: 84 | self._reactor.callFromThread(deferred.callback, result) 85 | 86 | def submit(self, work): 87 | """ 88 | Submit some work to be run. 89 | 90 | @param work: a 0-argument callable, which will be run in a thread. 91 | 92 | @return: L{Deferred} that fires with the result of L{work} 93 | """ 94 | if self._state not in (_STATE_RUNNING, _STATE_STARTING): 95 | raise RuntimeError("not running") 96 | d = Deferred() 97 | self._q.put((d, work)) 98 | return d 99 | 100 | def start(self): 101 | """ 102 | Start this thing, if it's stopped. 103 | """ 104 | if self._state != _STATE_STOPPED: 105 | raise RuntimeError("Not stopped.") 106 | self._state = _STATE_STARTING 107 | self._q = Queue(0) 108 | self._reactor.callInThread(self._run) 109 | self.retry() 110 | 111 | def retry(self): 112 | if self._state == _STATE_STARTING: 113 | if self._retryCallback is not None: 114 | self._reactor.threadpool.adjustPoolsize() 115 | self._retryCallback = self._reactor.callLater(0.1, self.retry) 116 | else: 117 | self._retryCallback = None 118 | 119 | def stop(self): 120 | """ 121 | Stop this thing and release its thread, if it's running. 122 | """ 123 | if self._state not in (_STATE_RUNNING, _STATE_STARTING): 124 | raise RuntimeError("Not running.") 125 | s = self._stopper = Deferred() 126 | self._state = _STATE_STOPPING 127 | self._q.put(_DONE) 128 | if self._retryCallback: 129 | self._retryCallback.cancel() 130 | self._retryCallback = None 131 | return s 132 | -------------------------------------------------------------------------------- /twext/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2009-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extentions to L{twisted.protocols}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/protocols/echo.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Echo protocol. 19 | """ 20 | 21 | __all__ = ["EchoProtocol"] 22 | 23 | from twisted.internet.protocol import Protocol 24 | 25 | 26 | class EchoProtocol(Protocol): 27 | """ 28 | Say what you hear. 29 | """ 30 | 31 | def dataReceived(self, data): 32 | """ 33 | As soon as any data is received, write it back. 34 | """ 35 | self.transport.write(data) 36 | -------------------------------------------------------------------------------- /twext/protocols/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2009-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.protocols}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/python/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Extensions to L{twisted.python}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/python/clsprop.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2011-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | A small utility for defining static class properties. 19 | """ 20 | 21 | __all__ = ["classproperty"] 22 | 23 | 24 | class classproperty(object): 25 | """ 26 | Decorator for a method that wants to return a static class property. The 27 | decorated method will only be invoked once, for each class, and that value 28 | will be returned for that class. 29 | """ 30 | 31 | def __init__(self, thunk=None, cache=True): 32 | self.cache = cache 33 | self.thunk = thunk 34 | self._classcache = {} 35 | 36 | def __call__(self, thunk): 37 | return self.__class__(thunk, self.cache) 38 | 39 | def __get__(self, instance, owner): 40 | if not self.cache: 41 | return self.thunk(owner) 42 | 43 | cc = self._classcache 44 | 45 | if owner in cc: 46 | cached = cc[owner] 47 | else: 48 | cached = self.thunk(owner) 49 | cc[owner] = cached 50 | 51 | return cached 52 | -------------------------------------------------------------------------------- /twext/python/filepath.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.python.test.test_filepath -*- 2 | ## 3 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | from __future__ import absolute_import 19 | 20 | """ 21 | Extend L{twisted.python.filepath} to provide performance enhancements for 22 | calendar server. 23 | """ 24 | 25 | __all__ = ["CachingFilePath"] 26 | 27 | from os import listdir as listdir 28 | from os.path import join, basename, exists, dirname 29 | from time import sleep 30 | from types import FunctionType, MethodType 31 | from errno import EINVAL 32 | from stat import S_ISDIR 33 | 34 | from twisted.python.filepath import FilePath as _FilePath 35 | 36 | 37 | class CachingFilePath(_FilePath, object): 38 | """ 39 | A descendent of L{_FilePath} which implements a more aggressive caching 40 | policy. 41 | """ 42 | 43 | _listdir = listdir # integration points for tests 44 | _sleep = sleep 45 | 46 | BACKOFF_MAX = 5.0 # Maximum time to wait between calls to listdir() 47 | 48 | def __init__(self, path, alwaysCreate=False): 49 | super(CachingFilePath, self).__init__(path, alwaysCreate) 50 | self.existsCached = None 51 | self.isDirCached = None 52 | 53 | @property 54 | def siblingExtensionSearch(self): 55 | """ 56 | Dynamically create a version of L{_FilePath.siblingExtensionSearch} 57 | that uses a pluggable L{listdir} implementation. 58 | """ 59 | return MethodType( 60 | FunctionType( 61 | _FilePath.siblingExtensionSearch.im_func.func_code, 62 | { 63 | "listdir": self._retryListdir, 64 | "basename": basename, 65 | "dirname": dirname, 66 | "joinpath": join, 67 | "exists": exists 68 | } 69 | ), 70 | self, self.__class__ 71 | ) 72 | 73 | def changed(self): 74 | """ 75 | This path may have changed in the filesystem, so forget all cached 76 | information about it. 77 | """ 78 | self.statinfo = None 79 | self.existsCached = None 80 | self.isDirCached = None 81 | 82 | def _retryListdir(self, pathname): 83 | """ 84 | Implementation of retry logic for C{listdir} and 85 | C{siblingExtensionSearch}. 86 | """ 87 | delay = 0.1 88 | 89 | while True: 90 | try: 91 | return self._listdir(pathname) 92 | except OSError, e: 93 | if e.errno == EINVAL: 94 | self._sleep(delay) 95 | delay = min(self.BACKOFF_MAX, delay * 2.0) 96 | else: 97 | raise 98 | 99 | raise AssertionError("unreachable code.") 100 | 101 | def listdir(self): 102 | """ 103 | List the directory which C{self.path} points to, compensating for 104 | EINVAL from C{os.listdir}. 105 | """ 106 | return self._retryListdir(self.path) 107 | 108 | def restat(self, reraise=True): 109 | """ 110 | Re-cache stat information. 111 | """ 112 | try: 113 | return super(CachingFilePath, self).restat(reraise) 114 | finally: 115 | if self.statinfo: 116 | self.existsCached = True 117 | self.isDirCached = S_ISDIR(self.statinfo.st_mode) 118 | else: 119 | self.existsCached = False 120 | self.isDirCached = None 121 | 122 | def moveTo(self, destination, followLinks=True): 123 | """ 124 | Override L{_FilePath.moveTo}, updating extended cache information if 125 | necessary. 126 | """ 127 | result = super(CachingFilePath, self).moveTo(destination, followLinks) 128 | self.changed() 129 | 130 | # Work with vanilla FilePath destinations to pacify the tests. 131 | if hasattr(destination, "changed"): 132 | destination.changed() 133 | 134 | return result 135 | 136 | def remove(self): 137 | """ 138 | Override L{_FilePath.remove}, updating extended cache information if 139 | necessary. 140 | """ 141 | try: 142 | return super(CachingFilePath, self).remove() 143 | finally: 144 | self.changed() 145 | 146 | 147 | CachingFilePath.clonePath = CachingFilePath 148 | -------------------------------------------------------------------------------- /twext/python/launchd.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.python.test.test_launchd -*- 2 | ## 3 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Binding for launchd socket hand-off API. 20 | """ 21 | 22 | from __future__ import print_function 23 | 24 | from cffi import FFI, VerificationError 25 | 26 | ffi = FFI() 27 | 28 | ffi.cdef( 29 | """ 30 | int launch_activate_socket(const char *name, int **fds, size_t *cnt); 31 | """ 32 | ) 33 | 34 | try: 35 | lib = ffi.verify( 36 | """ 37 | #include 38 | """, 39 | tag=__name__.replace(".", "_") 40 | ) 41 | except VerificationError as ve: 42 | raise ImportError(ve) 43 | 44 | 45 | def launchActivateSocket(name): 46 | fdList = [] 47 | 48 | fds = ffi.new('int **') 49 | count = ffi.new('size_t *') 50 | result = lib.launch_activate_socket(name, fds, count) 51 | if result == 0: 52 | for i in xrange(count[0]): 53 | fdList.append(fds[0][i]) 54 | return fdList 55 | 56 | 57 | __all__ = [ 58 | 'launchActivateSocket', 59 | ] 60 | -------------------------------------------------------------------------------- /twext/python/log.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.python.test.test_log-*- 2 | ## 3 | # Copyright (c) 2006-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | from twisted.logger import Logger as _Logger, LogLevel, LogPublisher, \ 19 | FileLogObserver, FilteringLogObserver, LogLevelFilterPredicate, \ 20 | formatEventAsClassicLogText, formatTime 21 | from twisted.python import log 22 | from twisted import logger 23 | 24 | 25 | class Logger(_Logger): 26 | """ 27 | Logging object. 28 | """ 29 | 30 | filterPublisher = None 31 | filterObserver = None 32 | filterPredicate = LogLevelFilterPredicate(defaultLogLevel=LogLevel.info) 33 | 34 | logBeginner = None 35 | 36 | @classmethod 37 | def makeFilteredFileLogObserver(cls, stream, withTime=True): 38 | """ 39 | For a child process that has its stdout captured by the master process to be logged by the master, 40 | we strip out the time from the log entry since the master process will always add one. Setting 41 | C{withTime} to L{False} will ensure no time is generated. 42 | """ 43 | assert ( 44 | cls.filterPublisher is None and 45 | cls.filterObserver is None 46 | ), "Only call this once" 47 | 48 | timeFormat = formatTime if withTime else lambda _: u"" 49 | cls.filterObserver = FileLogObserver(stream, lambda event: formatEventAsClassicLogText(event, formatTime=timeFormat)) 50 | cls.filterPublisher = LogPublisher(cls.filterObserver) 51 | return cls.filterPublisher 52 | 53 | @classmethod 54 | def addFilteredObserver(cls, observer): 55 | log.addObserver(FilteringLogObserver( 56 | observer, 57 | [cls.filterPredicate] 58 | )) 59 | 60 | @classmethod 61 | def beginLoggingTo(cls, observers, redirectStandardIO=True): 62 | if cls.logBeginner: 63 | cls.logBeginner.beginLoggingTo(observers, redirectStandardIO=redirectStandardIO) 64 | 65 | def emit(self, level, format=None, **kwargs): 66 | """ 67 | Fix {Logger.emit} to work with our legacy use and handle utf-8 properly. 68 | """ 69 | 70 | # For old style logging kwargs will be empty but the format string could contain "{}", so 71 | # We turn the format string into a kwarg 72 | if len(kwargs) == 0: 73 | kwargs["msg"] = format 74 | format = "{msg}" 75 | 76 | # Assume that any L{bytes} in the format or kwargs are utf-8 encoded strings 77 | if format is not None and isinstance(format, bytes): 78 | format = format.decode("utf-8") 79 | for k, v in kwargs.items(): 80 | if isinstance(v, bytes): 81 | kwargs[k] = v.decode("utf-8") 82 | 83 | super(Logger, self).emit(level, format=format, **kwargs) 84 | 85 | def levels(self): 86 | """ 87 | Get the L{LogLevelFilterPredicate} associated with this logger. 88 | """ 89 | return self.filterPredicate 90 | 91 | 92 | # Always replace Twisted's legacy log beginner with one that does LogLevel filtering 93 | class FilteringLogBeginnerWrapper(object): 94 | 95 | def __init__(self, beginner): 96 | self.beginner = beginner 97 | 98 | def beginLoggingTo( 99 | self, observers, discardBuffer=False, redirectStandardIO=True 100 | ): 101 | new_observers = [] 102 | for observer in observers: 103 | new_observers.append(FilteringLogObserver(observer, [Logger.filterPredicate])) 104 | self.beginner.beginLoggingTo(new_observers, discardBuffer, redirectStandardIO) 105 | 106 | 107 | Logger.logBeginner = FilteringLogBeginnerWrapper(log.theLogPublisher._logBeginner) 108 | logger.globalLogBeginner = Logger.logBeginner 109 | -------------------------------------------------------------------------------- /twext/python/parallel.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.python.test.test_parallel -*- 2 | ## 3 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Utilities for parallelizing tasks. 20 | """ 21 | 22 | from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue 23 | 24 | 25 | class Parallelizer(object): 26 | """ 27 | Do some operation with a degree of parallelism, using a set of resources 28 | which may each only be used for one task at a time, given some underlying 29 | API that returns L{Deferreds}. 30 | 31 | @ivar available: A list of available resources from the C{resources} 32 | constructor parameter. 33 | 34 | @ivar busy: A list of resources which are currently being used by 35 | operations. 36 | """ 37 | 38 | def __init__(self, resources): 39 | """ 40 | Initialize a L{Parallelizer} with a list of objects that will be passed 41 | to the callables sent to L{Parallelizer.do}. 42 | 43 | @param resources: objects which may be of any arbitrary type. 44 | @type resources: C{list} 45 | """ 46 | self.available = list(resources) 47 | self.busy = [] 48 | self.activeDeferreds = [] 49 | 50 | @inlineCallbacks 51 | def do(self, operation): 52 | """ 53 | Call C{operation} with one of the resources in C{self.available}, 54 | removing that value for use by other callers of C{do} until the task 55 | performed by C{operation} is complete (in other words, the L{Deferred} 56 | returned by C{operation} has fired). 57 | 58 | @param operation: a 1-argument callable taking a resource from 59 | C{self.active} and returning a L{Deferred} when it's done using 60 | that resource. 61 | @type operation: C{callable} 62 | 63 | @return: a L{Deferred} that fires as soon as there are resources 64 | available such that this task can be I{started} - not completed. 65 | """ 66 | if not self.available: 67 | yield DeferredList(self.activeDeferreds, fireOnOneCallback=True, 68 | fireOnOneErrback=True) 69 | active = self.available.pop(0) 70 | self.busy.append(active) 71 | o = operation(active) 72 | 73 | def andFinally(whatever): 74 | self.activeDeferreds.remove(o) 75 | self.busy.remove(active) 76 | self.available.append(active) 77 | return whatever 78 | self.activeDeferreds.append(o) 79 | o.addBoth(andFinally) 80 | returnValue(None) 81 | 82 | def done(self): 83 | """ 84 | Wait until all operations started by L{Parallelizer.do} are completed. 85 | 86 | @return: a L{Deferred} that fires (with C{None}) when all the currently 87 | pending work on this L{Parallelizer} is completed and C{busy} is 88 | empty again. 89 | """ 90 | return (DeferredList(self.activeDeferreds) 91 | .addCallback(lambda ignored: None)) 92 | -------------------------------------------------------------------------------- /twext/python/sacl.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | from __future__ import print_function 18 | 19 | __all__ = [ 20 | "checkSACL" 21 | ] 22 | 23 | from cffi import FFI, VerificationError 24 | 25 | ffi = FFI() 26 | 27 | definitions = """ 28 | typedef unsigned char uuid_t[16]; 29 | int mbr_check_service_membership(const uuid_t user, const char* servicename, int* ismember); 30 | int mbr_user_name_to_uuid(const char* name, uuid_t uu); 31 | int mbr_group_name_to_uuid(const char* name, uuid_t uu); 32 | """ 33 | 34 | ffi.cdef(definitions) 35 | 36 | try: 37 | lib = ffi.verify( 38 | definitions, 39 | libraries=[], 40 | tag=__name__.replace(".", "_") 41 | ) 42 | except VerificationError as ve: 43 | raise ImportError(ve) 44 | 45 | 46 | def checkSACL(userOrGroupName, serviceName): 47 | """ 48 | Check to see if a given user or group is a member of an OS X Server 49 | service's access group. If userOrGroupName is an empty string, we 50 | want to know if unauthenticated access is allowed for the given service. 51 | 52 | @param userOrGroupName: the name of the user or group 53 | @type userOrGroupName: C{unicode} 54 | 55 | @param serviceName: the name of the service (e.g. calendar, addressbook) 56 | @type serviceName: C{str} 57 | 58 | @return: True if the user or group is allowed access to service 59 | @rtype: C{bool} 60 | """ 61 | 62 | userOrGroupName = userOrGroupName.encode("utf-8") 63 | prefix = "com.apple.access_" 64 | uu = ffi.new("uuid_t") 65 | 66 | # See if the access group exists. If it does not, then there are no 67 | # restrictions 68 | groupName = prefix + serviceName 69 | groupMissing = lib.mbr_group_name_to_uuid(groupName, uu) 70 | if groupMissing: 71 | return True 72 | 73 | # See if userOrGroupName matches a user 74 | result = lib.mbr_user_name_to_uuid(userOrGroupName, uu) 75 | if result: 76 | # Not a user, try looking up a group of that name 77 | result = lib.mbr_group_name_to_uuid(userOrGroupName, uu) 78 | 79 | if result: 80 | # Neither a user nor a group matches the name 81 | return False 82 | 83 | # See if the uuid is a member of the service access group 84 | isMember = ffi.new("int *") 85 | result = lib.mbr_check_service_membership(uu, serviceName, isMember) 86 | if not result and isMember[0]: 87 | return True 88 | 89 | return False 90 | -------------------------------------------------------------------------------- /twext/python/sendfd.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.python.test.test_sendmsg -*- 2 | ## 3 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | from struct import pack, unpack, calcsize 19 | from socket import SOL_SOCKET 20 | 21 | from twisted.python.sendmsg import send1msg, recv1msg, SCM_RIGHTS 22 | 23 | 24 | def sendfd(socketfd, fd, description): 25 | """ 26 | Send the given FD to another process via L{send1msg} on the given 27 | C{AF_UNIX} socket. 28 | 29 | @param socketfd: An C{AF_UNIX} socket, attached to another process waiting 30 | to receive sockets via the ancillary data mechanism in L{send1msg}. 31 | 32 | @type socketfd: C{int} 33 | 34 | @param fd: A file descriptor to be sent to the other process. 35 | 36 | @type fd: C{int} 37 | 38 | @param description: a string describing the socket that was passed. 39 | 40 | @type description: C{str} 41 | """ 42 | send1msg( 43 | socketfd, description, 0, [(SOL_SOCKET, SCM_RIGHTS, pack("i", fd))] 44 | ) 45 | 46 | 47 | def recvfd(socketfd): 48 | """ 49 | Receive a file descriptor from a L{send1msg} message on the given 50 | C{AF_UNIX} socket. 51 | 52 | @param socketfd: An C{AF_UNIX} socket, attached to another process waiting 53 | to send sockets via the ancillary data mechanism in L{send1msg}. 54 | 55 | @param fd: C{int} 56 | 57 | @return: a 2-tuple of (new file descriptor, description). 58 | 59 | @rtype: 2-tuple of (C{int}, C{str}) 60 | """ 61 | data, _ignore_flags, ancillary = recv1msg(socketfd) 62 | [(_ignore_cmsg_level, _ignore_cmsg_type, packedFD)] = ancillary 63 | 64 | # cmsg_level and cmsg_type really need to be SOL_SOCKET / SCM_RIGHTS, but 65 | # since those are the *only* standard values, there's not much point in 66 | # checking. 67 | unpackedFD = 0 68 | int_size = calcsize("i") 69 | 70 | if len(packedFD) > int_size: # [ar]happens on 64 bit arch (FreeBSD) 71 | [unpackedFD] = unpack("i", packedFD[0:int_size]) 72 | else: 73 | [unpackedFD] = unpack("i", packedFD) 74 | 75 | return (unpackedFD, data) 76 | -------------------------------------------------------------------------------- /twext/python/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.python}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/python/test/pullpipe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- test-case-name: twext.python.test.test_sendmsg -*- 3 | ## 4 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ## 18 | 19 | if __name__ == '__main__': 20 | from twext.python.sendfd import recvfd 21 | import sys 22 | import os 23 | fd, description = recvfd(int(sys.argv[1])) 24 | os.write(fd, "Test fixture data: %s.\n" % (description,)) 25 | os.close(fd) 26 | -------------------------------------------------------------------------------- /twext/python/test/test_filepath.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for specialized behavior of L{CachingFilePath} 19 | """ 20 | from errno import EINVAL 21 | from os.path import join as pathjoin 22 | 23 | from twisted.internet.task import Clock 24 | 25 | from twisted.trial.unittest import TestCase 26 | 27 | from twext.python.filepath import CachingFilePath 28 | 29 | # Cheat and pull in the Twisted test cases for FilePath. XXX: Twisteds should 30 | # provide a supported way of doing this for exported interfaces. Also, it 31 | # should export IFilePath. --glyph 32 | 33 | # from twisted.test.test_paths import FilePathTests 34 | 35 | # class BaseVerification(FilePathTests): 36 | # """ 37 | # Make sure that L{CachingFilePath} doesn't break the contracts that 38 | # L{FilePath} tries to provide. 39 | # """ 40 | 41 | # def setUp(self): 42 | # """ 43 | # Set up the test case to set the base attributes to point at 44 | # L{AbstractFilePathTestCase}. 45 | # """ 46 | # FilePathTests.setUp(self) 47 | # self.root = CachingFilePath(self.root.path) 48 | # self.path = CachingFilePath(self.path.path) 49 | 50 | 51 | class EINVALTestCase(TestCase): 52 | """ 53 | Sometimes, L{os.listdir} will raise C{EINVAL}. This is a transient error, 54 | and L{CachingFilePath.listdir} should work around it by retrying the 55 | C{listdir} operation until it succeeds. 56 | """ 57 | 58 | def setUp(self): 59 | """ 60 | Create a L{CachingFilePath} for the test to use. 61 | """ 62 | self.cfp = CachingFilePath(self.mktemp()) 63 | self.clock = Clock() 64 | self.cfp._sleep = self.clock.advance 65 | 66 | def test_testValidity(self): 67 | """ 68 | If C{listdir} is replaced on a L{CachingFilePath}, we should be able to 69 | observe exceptions raised by the replacement. This verifies that the 70 | test patching done here is actually testing something. 71 | """ 72 | class CustomException(Exception): 73 | "Just for testing." 74 | def blowUp(dirname): 75 | raise CustomException() 76 | self.cfp._listdir = blowUp 77 | self.assertRaises(CustomException, self.cfp.listdir) 78 | self.assertRaises(CustomException, self.cfp.children) 79 | 80 | def test_retryLoop(self): 81 | """ 82 | L{CachingFilePath} should catch C{EINVAL} and respond by retrying the 83 | C{listdir} operation until it succeeds. 84 | """ 85 | calls = [] 86 | 87 | def raiseEINVAL(dirname): 88 | calls.append(dirname) 89 | if len(calls) < 5: 90 | raise OSError(EINVAL, "This should be caught by the test.") 91 | return ['a', 'b', 'c'] 92 | self.cfp._listdir = raiseEINVAL 93 | self.assertEquals(self.cfp.listdir(), ['a', 'b', 'c']) 94 | self.assertEquals(self.cfp.children(), [ 95 | CachingFilePath(pathjoin(self.cfp.path, 'a')), 96 | CachingFilePath(pathjoin(self.cfp.path, 'b')), 97 | CachingFilePath(pathjoin(self.cfp.path, 'c')), 98 | ]) 99 | 100 | def requireTimePassed(self, filenames): 101 | """ 102 | Create a replacement for listdir() which only fires after a certain 103 | amount of time. 104 | """ 105 | self.calls = [] 106 | 107 | def thunk(dirname): 108 | now = self.clock.seconds() 109 | if now < 20.0: 110 | self.calls.append(now) 111 | raise OSError(EINVAL, "Not enough time has passed yet.") 112 | else: 113 | return filenames 114 | self.cfp._listdir = thunk 115 | 116 | def assertRequiredTimePassed(self): 117 | """ 118 | Assert that calls to the simulated time.sleep() installed by 119 | C{requireTimePassed} have been invoked the required number of times. 120 | """ 121 | # Waiting should be growing by *2 each time until the additional wait 122 | # exceeds BACKOFF_MAX (5), at which point we should wait for 5s each 123 | # time. 124 | def cumulative(values): 125 | current = 0.0 126 | for value in values: 127 | current += value 128 | yield current 129 | 130 | self.assertEquals( 131 | self.calls, 132 | list(cumulative( 133 | [0.0, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 5.0, 5.0] 134 | )) 135 | ) 136 | 137 | def test_backoff(self): 138 | """ 139 | L{CachingFilePath} will wait for an increasing interval up to 140 | C{BACKOFF_MAX} between calls to listdir(). 141 | """ 142 | self.requireTimePassed(['a', 'b', 'c']) 143 | self.assertEquals(self.cfp.listdir(), ['a', 'b', 'c']) 144 | 145 | # def test_siblingExtensionSearch(self): 146 | # """ 147 | # L{FilePath.siblingExtensionSearch} is unfortunately not implemented in 148 | # terms of L{FilePath.listdir}, so we need to verify that it will also 149 | # retry. 150 | # """ 151 | # filenames = [self.cfp.basename() + '.a', 152 | # self.cfp.basename() + '.b', 153 | # self.cfp.basename() + '.c'] 154 | # siblings = map(self.cfp.sibling, filenames) 155 | # for sibling in siblings: 156 | # sibling.touch() 157 | # self.requireTimePassed(filenames) 158 | # self.assertEquals(self.cfp.siblingExtensionSearch("*"), 159 | # siblings[0]) 160 | # self.assertRequiredTimePassed() 161 | -------------------------------------------------------------------------------- /twext/python/test/test_launchd.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.python.launchd}. 19 | """ 20 | 21 | from subprocess import Popen, PIPE 22 | import json 23 | import os 24 | import plistlib 25 | import socket 26 | import sys 27 | 28 | if __name__ == '__main__': 29 | # This module is loaded as a launchd job by test-cases below; the following 30 | # code looks up an appropriate function to run. 31 | testID = sys.argv[1] 32 | a, b = testID.rsplit(".", 1) 33 | from twisted.python.reflect import namedAny 34 | try: 35 | namedAny(".".join([a, b.replace("test_", "job_")]))() 36 | finally: 37 | sys.stdout.flush() 38 | sys.stderr.flush() 39 | skt = socket.socket() 40 | skt.connect(("127.0.0.1", int(os.environ["TESTING_PORT"]))) 41 | sys.exit(0) 42 | 43 | 44 | try: 45 | from twext.python.launchd import launchActivateSocket 46 | except ImportError: 47 | skip = "LaunchD not available." 48 | else: 49 | skip = False 50 | 51 | from twisted.trial.unittest import TestCase, SkipTest 52 | from twisted.python.filepath import FilePath 53 | 54 | 55 | class CheckInTests(TestCase): 56 | """ 57 | Integration tests making sure that actual checkin with launchd results in 58 | the expected values. 59 | """ 60 | 61 | def setUp(self): 62 | fp = FilePath(self.mktemp()) 63 | fp.makedirs() 64 | from twisted.internet.protocol import Protocol, Factory 65 | from twisted.internet import reactor, defer 66 | d = defer.Deferred() 67 | 68 | class JustLetMeMoveOn(Protocol): 69 | 70 | def connectionMade(self): 71 | d.callback(None) 72 | self.transport.abortConnection() 73 | f = Factory() 74 | f.protocol = JustLetMeMoveOn 75 | port = reactor.listenTCP(0, f, interface="127.0.0.1") 76 | 77 | @self.addCleanup 78 | def goodbyePort(): 79 | return port.stopListening() 80 | env = dict(os.environ) 81 | env["TESTING_PORT"] = repr(port.getHost().port) 82 | self.stdout = fp.child("stdout.txt") 83 | self.stderr = fp.child("stderr.txt") 84 | self.launchLabel = ("org.calendarserver.UNIT-TESTS." + 85 | str(os.getpid()) + "." + self.id()) 86 | plist = { 87 | "Label": self.launchLabel, 88 | "ProgramArguments": [sys.executable, "-m", __name__, self.id()], 89 | "EnvironmentVariables": env, 90 | "KeepAlive": False, 91 | "StandardOutPath": self.stdout.path, 92 | "StandardErrorPath": self.stderr.path, 93 | "Sockets": { 94 | "Awesome": [{"SecureSocketWithKey": "GeneratedSocket"}] 95 | }, 96 | "RunAtLoad": True, 97 | "Debug": True, 98 | } 99 | self.job = fp.child("job.plist") 100 | self.job.setContent(plistlib.writePlistToString(plist)) 101 | 102 | child = Popen( 103 | args=[ 104 | "launchctl", 105 | "load", 106 | self.job.path, 107 | ], 108 | stdout=PIPE, stderr=PIPE, 109 | ) 110 | _ignore_output, error = child.communicate() 111 | 112 | if child.returncode != 0 or error: 113 | raise SkipTest("launchctl cannot run on this system") 114 | 115 | return d 116 | 117 | @staticmethod 118 | def job_test(): 119 | """ 120 | Do something observable in a subprocess. 121 | """ 122 | sys.stdout.write("Sample Value.") 123 | sys.stdout.flush() 124 | 125 | def test_test(self): 126 | """ 127 | Since this test framework is somewhat finicky, let's just make sure 128 | that a test can complete. 129 | """ 130 | self.assertEquals("Sample Value.", self.stdout.getContent()) 131 | 132 | @staticmethod 133 | def job_getFDs(): 134 | """ 135 | Check-in via the high-level C{getLaunchDSocketFDs} API, that just gives 136 | us listening FDs. 137 | """ 138 | sys.stdout.write(json.dumps(launchActivateSocket("Awesome"))) 139 | 140 | def test_getFDs(self): 141 | """ 142 | L{getLaunchDSocketFDs} returns a Python dictionary mapping the names of 143 | sockets specified in the property list to lists of integers 144 | representing FDs. 145 | """ 146 | sockets = json.loads(self.stdout.getContent()) 147 | self.assertEquals(len(sockets), 1) 148 | self.assertIsInstance(sockets[0], int) 149 | 150 | def tearDown(self): 151 | """ 152 | Un-load the launchd job and report any errors it encountered. 153 | """ 154 | os.spawnlp(os.P_WAIT, "launchctl", 155 | "launchctl", "unload", self.job.path) 156 | err = self.stderr.getContent() 157 | if 'Traceback' in err: 158 | self.fail(err) 159 | -------------------------------------------------------------------------------- /twext/python/test/test_log.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2005-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | from cStringIO import StringIO 18 | from twext.python.log import Logger 19 | from twisted.logger import LogLevel, LogPublisher, textFileLogObserver 20 | from twisted.trial import unittest 21 | 22 | 23 | class LogPublisherTests(unittest.TestCase): 24 | """ 25 | Tests for L{Logger}. 26 | """ 27 | 28 | def test_old_style(self): 29 | """ 30 | L{Logger} handles old style log strings. 31 | """ 32 | observer = LogPublisher() 33 | 34 | observed = [] 35 | observer.addObserver(observed.append) 36 | 37 | sio = StringIO() 38 | observer.addObserver(textFileLogObserver(sio)) 39 | 40 | logger = Logger(observer=observer) 41 | 42 | index = 0 43 | logger.info("test") 44 | self.assertEqual(observed[index]["log_level"], LogLevel.info) 45 | self.assertEqual(observed[index]["log_format"], u"{msg}") 46 | self.assertEqual(observed[index]["msg"], u"test") 47 | self.assertEqual(sio.getvalue().splitlines()[index].split("#info] ")[1], "test") 48 | 49 | index += 1 50 | logger.info("test {}") 51 | self.assertEqual(observed[index]["log_level"], LogLevel.info) 52 | self.assertEqual(observed[index]["log_format"], u"{msg}") 53 | self.assertEqual(observed[index]["msg"], u"test {}") 54 | self.assertEqual(sio.getvalue().splitlines()[index].split("#info] ")[1], "test {}") 55 | 56 | index += 1 57 | logger.info("test {foo}") 58 | self.assertEqual(observed[index]["log_level"], LogLevel.info) 59 | self.assertEqual(observed[index]["log_format"], u"{msg}") 60 | self.assertEqual(observed[index]["msg"], u"test {foo}") 61 | self.assertEqual(sio.getvalue().splitlines()[index].split("#info] ")[1], "test {foo}") 62 | 63 | def test_utf8(self): 64 | """ 65 | L{Logger} handles utf8 log strings and format args. 66 | """ 67 | observer = LogPublisher() 68 | 69 | observed = [] 70 | observer.addObserver(observed.append) 71 | 72 | sio = StringIO() 73 | observer.addObserver(textFileLogObserver(sio)) 74 | 75 | logger = Logger(observer=observer) 76 | 77 | index = 0 78 | logger.info("t\xc3\xa9st") 79 | self.assertEqual(observed[index]["log_level"], LogLevel.info) 80 | self.assertEqual(observed[index]["log_format"], u"{msg}") 81 | self.assertEqual(observed[index]["msg"], u"t\xe9st") 82 | self.assertEqual(sio.getvalue().splitlines()[index].split("#info] ")[1], "t\xc3\xa9st") 83 | 84 | index += 1 85 | logger.info("{str}", str="t\xc3\xa9st") 86 | self.assertEqual(observed[index]["log_level"], LogLevel.info) 87 | self.assertEqual(observed[index]["log_format"], u"{str}") 88 | self.assertEqual(observed[index]["str"], u"t\xe9st") 89 | self.assertEqual(sio.getvalue().splitlines()[index].split("#info] ")[1], "t\xc3\xa9st") 90 | 91 | index += 1 92 | logger.info("T\xc3\xa9st {str}", str="t\xc3\xa9st") 93 | self.assertEqual(observed[index]["log_level"], LogLevel.info) 94 | self.assertEqual(observed[index]["log_format"], u"T\xe9st {str}") 95 | self.assertEqual(observed[index]["str"], u"t\xe9st") 96 | self.assertEqual(sio.getvalue().splitlines()[index].split("#info] ")[1], "T\xc3\xa9st t\xc3\xa9st") 97 | -------------------------------------------------------------------------------- /twext/python/test/test_parallel.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2012-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.python.parallel}. 19 | """ 20 | 21 | from twisted.internet.defer import Deferred 22 | 23 | from twext.python.parallel import Parallelizer 24 | 25 | from twisted.trial.unittest import TestCase 26 | 27 | 28 | class ParallelizerTests(TestCase): 29 | """ 30 | Tests for L{Parallelizer}. 31 | """ 32 | 33 | def test_doAndDone(self): 34 | """ 35 | Blanket catch-all test. (TODO: split this up into more nice 36 | fine-grained tests.) 37 | """ 38 | d1 = Deferred() 39 | d2 = Deferred() 40 | d3 = Deferred() 41 | d4 = Deferred() 42 | doing = [] 43 | done = [] 44 | allDone = [] 45 | p = Parallelizer(['a', 'b', 'c']) 46 | p.do(lambda a: doing.append(a) or d1).addCallback(done.append) 47 | p.do(lambda b: doing.append(b) or d2).addCallback(done.append) 48 | p.do(lambda c: doing.append(c) or d3).addCallback(done.append) 49 | p.do(lambda b1: doing.append(b1) or d4).addCallback(done.append) 50 | p.done().addCallback(allDone.append) 51 | self.assertEqual(allDone, []) 52 | self.assertEqual(doing, ['a', 'b', 'c']) 53 | self.assertEqual(done, [None, None, None]) 54 | d2.callback(1) 55 | self.assertEqual(doing, ['a', 'b', 'c', 'b']) 56 | self.assertEqual(done, [None, None, None, None]) 57 | self.assertEqual(allDone, []) 58 | d3.callback(2) 59 | d4.callback(3) 60 | d1.callback(4) 61 | self.assertEqual(done, [None, None, None, None]) 62 | self.assertEqual(allDone, [None]) 63 | -------------------------------------------------------------------------------- /twext/python/types.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2011-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Utilities related to types. 19 | """ 20 | 21 | __all__ = [ 22 | "MappingProxyType", 23 | ] 24 | 25 | 26 | class MappingProxyType(object): 27 | """ 28 | Read-only proxy of a mapping. It provides a dynamic view on the mapping's 29 | entries, which means that when the mapping changes, the view reflects these 30 | changes. 31 | 32 | Note that for an ummutable mapping, one would have to prevent modifications 33 | to the underlying mapping. Note also that mutable values remain mutable 34 | when accessed via a proxy. 35 | 36 | Backport of Python 3's L{types.MappingProxyType 37 | }. 38 | """ 39 | 40 | def __init__(self, mapping): 41 | """ 42 | @param mapping: A mapping to wrap. 43 | @type mapping: mapping 44 | """ 45 | self._mapping = mapping 46 | 47 | def __len__(self): 48 | return len(self._mapping) 49 | 50 | def __getitem__(self, key): 51 | return self._mapping[key] 52 | 53 | def __iter__(self): 54 | return iter(self._mapping) 55 | 56 | def __reversed__(self): 57 | return reversed(self._mapping) 58 | 59 | def __contains__(self, key): 60 | return key in self._mapping 61 | 62 | def copy(self): 63 | return self._mapping.copy() 64 | 65 | def get(self, key, default=None): 66 | return self._mapping.get(key, default) 67 | 68 | def has_key(self, key): 69 | return key in self._mapping 70 | 71 | def items(self): 72 | return self._mapping.items() 73 | 74 | def iteritems(self): 75 | return self._mapping.iteritems() 76 | 77 | def iterkeys(self): 78 | return self._mapping.iterkeys() 79 | 80 | def itervalues(self): 81 | return self._mapping.itervalues() 82 | 83 | def keys(self): 84 | return self._mapping.keys() 85 | 86 | def values(self): 87 | return self._mapping.values() 88 | 89 | def viewitems(self): 90 | return self._mapping.viewitems() 91 | 92 | def viewkeys(self): 93 | return self._mapping.viewkeys() 94 | 95 | def viewvalues(self): 96 | return self._mapping.viewvalues() 97 | -------------------------------------------------------------------------------- /twext/who/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.who.test -*- 2 | ## 3 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Directory service integration. 20 | """ 21 | -------------------------------------------------------------------------------- /twext/who/aggregate.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.who.test.test_aggregate -*- 2 | ## 3 | # Copyright (c) 2006-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Directory service which aggregates multiple directory services. 20 | """ 21 | 22 | __all__ = [ 23 | "DirectoryService", 24 | ] 25 | 26 | import collections 27 | from itertools import chain 28 | 29 | from twisted.python.components import proxyForInterface 30 | from twisted.internet.defer import ( 31 | gatherResults, FirstError, succeed, inlineCallbacks, returnValue 32 | ) 33 | 34 | from .idirectory import DirectoryConfigurationError, IDirectoryService 35 | from .directory import ( 36 | DirectoryService as BaseDirectoryService 37 | ) 38 | from .util import ConstantsContainer 39 | 40 | 41 | class DirectoryService(BaseDirectoryService): 42 | """ 43 | Aggregate directory service. 44 | """ 45 | 46 | def __init__(self, realmName, services): 47 | recordTypes = set() 48 | 49 | for service in services: 50 | if not IDirectoryService.implementedBy(service.__class__): 51 | raise ValueError( 52 | "Not a directory service: {0}".format(service) 53 | ) 54 | 55 | service = proxyForInterface( 56 | IDirectoryService, originalAttribute="_service" 57 | )(service) 58 | 59 | for recordType in service.recordTypes(): 60 | if recordType in recordTypes: 61 | raise DirectoryConfigurationError( 62 | "Aggregated services may not vend " 63 | "the same record type: {0}" 64 | .format(recordType) 65 | ) 66 | recordTypes.add(recordType) 67 | 68 | BaseDirectoryService.__init__(self, realmName) 69 | 70 | self._services = tuple(services) 71 | 72 | @property 73 | def services(self): 74 | return self._services 75 | 76 | @property 77 | def recordType(self): 78 | if not hasattr(self, "_recordType"): 79 | self._recordType = ConstantsContainer(tuple( 80 | s.recordTypes() 81 | for s in self.services 82 | )) 83 | return self._recordType 84 | 85 | @inlineCallbacks 86 | def _oneFromSubServices(self, methodName, *args, **kwargs): 87 | for service in self.services: 88 | m = getattr(service, methodName) 89 | record = yield m(*args, **kwargs) 90 | 91 | if record is not None: 92 | returnValue(record) 93 | 94 | returnValue(None) 95 | 96 | def _gatherFromSubServices(self, methodName, *args, **kwargs): 97 | ds = [] 98 | for service in self.services: 99 | m = getattr(service, methodName) 100 | d = m(*args, **kwargs) 101 | ds.append(d) 102 | 103 | def unwrapFirstError(f): 104 | f.trap(FirstError) 105 | return f.value.subFailure 106 | 107 | d = gatherResults(ds, consumeErrors=True) 108 | d.addCallback(lambda results: chain(*results)) 109 | d.addErrback(unwrapFirstError) 110 | return d 111 | 112 | def recordsFromExpression( 113 | self, expression, recordTypes=None, records=None, 114 | limitResults=None, timeoutSeconds=None 115 | ): 116 | return self._gatherFromSubServices( 117 | "recordsFromExpression", expression, recordTypes=recordTypes, 118 | records=None, 119 | limitResults=limitResults, timeoutSeconds=timeoutSeconds 120 | ) 121 | 122 | # Implementation of recordWith*() methods may seem unnecessary here, since 123 | # they eventually end up at recordsFromExpression() anyway (in our 124 | # superclass). 125 | # However, the wrapped services may have optimzed versions of these, so we 126 | # want to call their implementations, not bypass them. 127 | 128 | def recordsWithFieldValue( 129 | self, fieldName, value, limitResults=None, timeoutSeconds=None 130 | ): 131 | return self._gatherFromSubServices( 132 | "recordsWithFieldValue", fieldName, value, 133 | limitResults=limitResults, timeoutSeconds=timeoutSeconds 134 | ) 135 | 136 | def recordWithUID(self, uid, timeoutSeconds=None): 137 | return self._oneFromSubServices( 138 | "recordWithUID", uid, timeoutSeconds=timeoutSeconds 139 | ) 140 | 141 | def recordWithGUID(self, guid, timeoutSeconds=None): 142 | return self._oneFromSubServices( 143 | "recordWithGUID", guid, timeoutSeconds=timeoutSeconds 144 | ) 145 | 146 | def recordsWithRecordType( 147 | self, recordType, limitResults=None, timeoutSeconds=None 148 | ): 149 | # Since we know the recordType, we can go directly to the appropriate 150 | # service. 151 | for service in self.services: 152 | if recordType in service.recordTypes(): 153 | return service.recordsWithRecordType( 154 | recordType, 155 | limitResults=limitResults, timeoutSeconds=timeoutSeconds 156 | ) 157 | return succeed(()) 158 | 159 | def recordWithShortName(self, recordType, shortName, timeoutSeconds=None): 160 | # Since we know the recordType, we can go directly to the appropriate 161 | # service. 162 | for service in self.services: 163 | if recordType in service.recordTypes(): 164 | return service.recordWithShortName( 165 | recordType, shortName, timeoutSeconds=timeoutSeconds 166 | ) 167 | return succeed(None) 168 | 169 | def recordsWithEmailAddress( 170 | self, emailAddress, limitResults=None, timeoutSeconds=None 171 | ): 172 | return self._gatherFromSubServices( 173 | "recordsWithEmailAddress", emailAddress, 174 | limitResults=limitResults, timeoutSeconds=timeoutSeconds 175 | ) 176 | 177 | @inlineCallbacks 178 | def updateRecords(self, records, create=False): 179 | # When migrating there may be lots of new records so batch this by each 180 | # service record type. 181 | recordsByType = collections.defaultdict(list) 182 | for record in records: 183 | recordsByType[record.recordType].append(record) 184 | 185 | for recordType, recordList in recordsByType.items(): 186 | for service in self.services: 187 | if recordType in service.recordTypes(): 188 | yield service.updateRecords(recordList, create=create) 189 | 190 | @inlineCallbacks 191 | def removeRecords(self, uids): 192 | # FIXME: since we don't know which sub-service owns each uid, we 193 | # currently try removing the uids in each sub-service, ignoring 194 | # errors. 195 | for service in self.services: 196 | try: 197 | yield service.removeRecords(uids) 198 | except: 199 | pass 200 | 201 | @inlineCallbacks 202 | def flush(self): 203 | for service in self.services: 204 | try: 205 | yield service.flush() 206 | except: 207 | pass 208 | -------------------------------------------------------------------------------- /twext/who/checker.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.who.test.test_checker -*- 2 | ## 3 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | L{twisted.cred}-style credential checker. 20 | """ 21 | 22 | from zope.interface import implementer, classImplements, Attribute 23 | 24 | from twisted.internet.defer import inlineCallbacks, returnValue 25 | from twisted.cred.error import UnauthorizedLogin 26 | from twisted.cred.checkers import ICredentialsChecker 27 | from twisted.cred.credentials import ( 28 | ICredentials, IUsernamePassword, IUsernameHashedPassword, 29 | DigestedCredentials, 30 | ) 31 | 32 | from .idirectory import ( 33 | IDirectoryService, RecordType, 34 | IPlaintextPasswordVerifier, IHTTPDigestVerifier, 35 | ) 36 | 37 | 38 | @implementer(ICredentialsChecker) 39 | class BaseCredentialChecker(object): 40 | # credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword) 41 | 42 | def __init__(self, service): 43 | """ 44 | @param service: The directory service to use to obtain directory 45 | records and validate credentials against. 46 | @type service: L{IDirectoryService} 47 | """ 48 | if not IDirectoryService.providedBy(service): 49 | raise TypeError("Not an IDirectoryService: {0!r}".format(service)) 50 | 51 | self.service = service 52 | 53 | 54 | class UsernamePasswordCredentialChecker(BaseCredentialChecker): 55 | credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword) 56 | 57 | @inlineCallbacks 58 | def requestAvatarId(self, credentials): 59 | if not IUsernamePassword.providedBy(credentials): 60 | raise TypeError( 61 | "Not an IUsernamePassword: {0!r}".format(credentials) 62 | ) 63 | 64 | record = yield self.service.recordWithShortName( 65 | RecordType.user, credentials.username.decode("utf-8") 66 | ) 67 | 68 | if record is None: 69 | raise UnauthorizedLogin("No such user") 70 | 71 | if not IPlaintextPasswordVerifier.providedBy(record): 72 | raise UnauthorizedLogin( 73 | "Not an IPlaintextPasswordVerifier: {0!r}".format(record) 74 | ) 75 | 76 | auth = yield record.verifyPlaintextPassword(credentials.password) 77 | if auth: 78 | returnValue(record) 79 | 80 | raise UnauthorizedLogin("Incorrect password") 81 | 82 | 83 | class IHTTPDigest(ICredentials): 84 | """ 85 | HTTP digest credentials. 86 | """ 87 | username = Attribute("User (short) name") 88 | method = Attribute("...") 89 | fields = Attribute("...") # Attributes would be better. 90 | 91 | 92 | classImplements(DigestedCredentials, (IHTTPDigest,)) 93 | 94 | 95 | class HTTPDigestCredentialChecker(BaseCredentialChecker): 96 | credentialInterfaces = (IHTTPDigest,) 97 | 98 | @inlineCallbacks 99 | def requestAvatarId(self, credentials): 100 | if not IHTTPDigest.providedBy(credentials): 101 | raise TypeError( 102 | "Not an IHTTPDigest: {0!r}".format(credentials) 103 | ) 104 | 105 | record = yield self.service.recordWithShortName( 106 | RecordType.user, credentials.username.decode("utf-8") 107 | ) 108 | 109 | if record is None: 110 | raise UnauthorizedLogin("No such user") 111 | 112 | if not IHTTPDigestVerifier.providedBy(record): 113 | raise UnauthorizedLogin( 114 | "Not an IHTTPDigestVerifier: {0!r}".format(record) 115 | ) 116 | 117 | result = yield record.verifyHTTPDigest( 118 | credentials.username, 119 | credentials.fields.get("realm"), 120 | credentials.fields.get("uri"), 121 | credentials.fields.get("nonce"), 122 | credentials.fields.get("cnonce"), 123 | credentials.fields.get("algorithm", "md5"), 124 | credentials.fields.get("nc"), 125 | credentials.fields.get("qop"), 126 | credentials.fields.get("response"), 127 | credentials.method, 128 | ) 129 | if result: 130 | returnValue(record) 131 | 132 | raise UnauthorizedLogin("Incorrect password") 133 | -------------------------------------------------------------------------------- /twext/who/ldap/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2014-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | LDAP directory service. 19 | """ 20 | 21 | __all__ = [ 22 | "LDAPError", 23 | "LDAPConfigurationError", 24 | "LDAPConnectionError", 25 | "LDAPBindAuthError", 26 | "LDAPQueryError", 27 | "RecordTypeSchema", 28 | "DirectoryService", 29 | "LDAPAttribute", 30 | "LDAPObjectClass", 31 | "FieldName", 32 | ] 33 | 34 | 35 | from ._service import ( 36 | LDAPError, 37 | LDAPConfigurationError, 38 | LDAPConnectionError, 39 | LDAPBindAuthError, 40 | LDAPQueryError, 41 | RecordTypeSchema, 42 | DirectoryService, 43 | FieldName 44 | ) 45 | from ._constants import ( 46 | LDAPAttribute, 47 | LDAPObjectClass, 48 | ) 49 | -------------------------------------------------------------------------------- /twext/who/ldap/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2014-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | -------------------------------------------------------------------------------- /twext/who/opendirectory/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | OpenDirectory directory service. 19 | """ 20 | 21 | __all__ = [ 22 | "OpenDirectoryError", 23 | "OpenDirectoryConnectionError", 24 | "OpenDirectoryQueryError", 25 | "OpenDirectoryDataError", 26 | "DirectoryService", 27 | "NoQOPDigestCredentialFactory", 28 | "RecordType", 29 | ] 30 | 31 | 32 | from ._service import ( 33 | OpenDirectoryError, OpenDirectoryConnectionError, OpenDirectoryQueryError, 34 | OpenDirectoryDataError, 35 | DirectoryService, 36 | NoQOPDigestCredentialFactory, 37 | ) 38 | -------------------------------------------------------------------------------- /twext/who/opendirectory/_odframework.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | OpenDirectory.framework 19 | """ 20 | 21 | import objc as _objc 22 | 23 | __bundle__ = _objc.initFrameworkWrapper( 24 | "OpenDirectory", 25 | frameworkIdentifier="com.apple.OpenDirectory", 26 | frameworkPath=_objc.pathForFramework( 27 | "/System/Library/Frameworks/OpenDirectory.framework" 28 | ), 29 | globals=globals() 30 | ) 31 | -------------------------------------------------------------------------------- /twext/who/opendirectory/_scripts.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | import sys 18 | import md5 19 | import sha 20 | from getpass import getpass 21 | 22 | from twisted.internet.defer import inlineCallbacks 23 | from twisted.cred.credentials import UsernamePassword, DigestedCredentials 24 | from twisted.cred.error import UnauthorizedLogin 25 | 26 | from twext.who.expression import ( 27 | MatchExpression, MatchType, CompoundExpression, Operand, 28 | ) 29 | from twext.who.opendirectory import DirectoryService 30 | 31 | 32 | algorithms = { 33 | "md5": md5.new, 34 | "md5-sess": md5.new, 35 | "sha": sha.new, 36 | } 37 | 38 | 39 | # DigestCalcHA1 40 | def calcHA1( 41 | pszAlg, 42 | pszUserName, 43 | pszRealm, 44 | pszPassword, 45 | pszNonce, 46 | pszCNonce, 47 | preHA1=None 48 | ): 49 | """ 50 | @param pszAlg: The name of the algorithm to use to calculate the digest. 51 | Currently supported are md5 md5-sess and sha. 52 | 53 | @param pszUserName: The username 54 | @param pszRealm: The realm 55 | @param pszPassword: The password 56 | @param pszNonce: The nonce 57 | @param pszCNonce: The cnonce 58 | 59 | @param preHA1: If available this is a str containing a previously 60 | calculated HA1 as a hex string. If this is given then the values for 61 | pszUserName, pszRealm, and pszPassword are ignored. 62 | """ 63 | 64 | if (preHA1 and (pszUserName or pszRealm or pszPassword)): 65 | raise TypeError(("preHA1 is incompatible with the pszUserName, " 66 | "pszRealm, and pszPassword arguments")) 67 | 68 | if preHA1 is None: 69 | # We need to calculate the HA1 from the username:realm:password 70 | m = algorithms[pszAlg]() 71 | m.update(pszUserName) 72 | m.update(":") 73 | m.update(pszRealm) 74 | m.update(":") 75 | m.update(pszPassword) 76 | HA1 = m.digest() 77 | else: 78 | # We were given a username:realm:password 79 | HA1 = preHA1.decode("hex") 80 | 81 | if pszAlg == "md5-sess": 82 | m = algorithms[pszAlg]() 83 | m.update(HA1) 84 | m.update(":") 85 | m.update(pszNonce) 86 | m.update(":") 87 | m.update(pszCNonce) 88 | HA1 = m.digest() 89 | 90 | return HA1.encode("hex") 91 | 92 | 93 | # DigestCalcResponse 94 | def calcResponse( 95 | HA1, 96 | algo, 97 | pszNonce, 98 | pszNonceCount, 99 | pszCNonce, 100 | pszQop, 101 | pszMethod, 102 | pszDigestUri, 103 | pszHEntity, 104 | ): 105 | m = algorithms[algo]() 106 | m.update(pszMethod) 107 | m.update(":") 108 | m.update(pszDigestUri) 109 | if pszQop == "auth-int" or pszQop == "auth-conf": 110 | m.update(":") 111 | m.update(pszHEntity) 112 | HA2 = m.digest().encode("hex") 113 | 114 | m = algorithms[algo]() 115 | m.update(HA1) 116 | m.update(":") 117 | m.update(pszNonce) 118 | m.update(":") 119 | if pszNonceCount and pszCNonce and pszQop: 120 | m.update(pszNonceCount) 121 | m.update(":") 122 | m.update(pszCNonce) 123 | m.update(":") 124 | m.update(pszQop) 125 | m.update(":") 126 | m.update(HA2) 127 | respHash = m.digest().encode("hex") 128 | return respHash 129 | 130 | 131 | @inlineCallbacks 132 | def authUsernamePassword(username, password): 133 | # Authenticate using simple password 134 | 135 | service = DirectoryService() 136 | 137 | creds = UsernamePassword(username, password) 138 | try: 139 | id = yield service.requestAvatarId(creds) 140 | print("OK via UsernamePassword, avatarID: {id}".format(id=id)) 141 | print(" {name}".format(name=id.fullNames)) 142 | except UnauthorizedLogin: 143 | print("Via UsernamePassword, could not authenticate") 144 | 145 | print("") 146 | 147 | # Authenticate using Digest 148 | 149 | algorithm = "md5" # "md5-sess" 150 | cnonce = "/rrD6TqPA3lHRmg+fw/vyU6oWoQgzK7h9yWrsCmv/lE=" 151 | entity = "00000000000000000000000000000000" 152 | method = "GET" 153 | nc = "00000001" 154 | nonce = "128446648710842461101646794502" 155 | qop = None 156 | realm = "host.example.com" 157 | uri = "http://host.example.com" 158 | 159 | responseHash = calcResponse( 160 | calcHA1( 161 | algorithm.lower(), username, realm, password, nonce, cnonce 162 | ), 163 | algorithm.lower(), nonce, nc, cnonce, qop, method, uri, entity 164 | ) 165 | 166 | response = ( 167 | 'Digest username="{username}", uri="{uri}", response={hash}'.format( 168 | username=username, uri=uri, hash=responseHash 169 | ) 170 | ) 171 | 172 | fields = { 173 | "realm": realm, 174 | "nonce": nonce, 175 | "response": response, 176 | "algorithm": algorithm, 177 | } 178 | 179 | creds = DigestedCredentials(username, method, realm, fields) 180 | 181 | try: 182 | id = yield service.requestAvatarId(creds) 183 | print("OK via DigestedCredentials, avatarID: {id}".format(id=id)) 184 | print(" {name}".format(name=id.fullNames)) 185 | except UnauthorizedLogin: 186 | print("Via DigestedCredentials, could not authenticate") 187 | 188 | 189 | @inlineCallbacks 190 | def lookup(shortNames): 191 | service = DirectoryService() 192 | print( 193 | "Service = {service}\n" 194 | "Session = {service.session}\n" 195 | "Node = {service.node}\n" 196 | # "Local node = {service.localNode}\n" 197 | .format(service=service) 198 | ) 199 | print("-" * 80) 200 | 201 | for shortName in shortNames: 202 | print("Looking up short name: {0}".format(shortName)) 203 | 204 | record = yield service.recordWithShortName(service.recordType.user, shortName) 205 | if record: 206 | print(record.description()) 207 | 208 | continue 209 | 210 | matchExpression = MatchExpression( 211 | service.fieldName.shortNames, shortName, 212 | matchType=MatchType.equals, 213 | ) 214 | 215 | records = yield service.recordsFromExpression(matchExpression) 216 | for record in records: 217 | print(record.description()) 218 | 219 | compoundExpression = CompoundExpression( 220 | [ 221 | MatchExpression( 222 | service.fieldName.shortNames, shortName, 223 | matchType=MatchType.contains 224 | ), 225 | MatchExpression( 226 | service.fieldName.emailAddresses, shortName, 227 | matchType=MatchType.contains 228 | ), 229 | ], 230 | Operand.OR 231 | ) 232 | 233 | records = yield service.recordsFromExpression(compoundExpression) 234 | for record in records: 235 | print(record.description()) 236 | 237 | 238 | def run_auth(): 239 | username = raw_input("Username: ") 240 | if username: 241 | password = getpass() 242 | if password: 243 | authUsernamePassword(username, password) 244 | 245 | 246 | def run_lookup(): 247 | lookup(sys.argv[1:]) 248 | -------------------------------------------------------------------------------- /twext/who/opendirectory/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | -------------------------------------------------------------------------------- /twext/who/test/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Tests for L{twext.who}. 19 | """ 20 | -------------------------------------------------------------------------------- /twext/who/test/auth_resource.rpy: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | # 18 | # Run: 19 | # twistd -n web --path twext/who/test 20 | # 21 | # And open this URL: 22 | # http://localhost:8080/auth_resource.rpy 23 | # 24 | 25 | cache() 26 | 27 | from tempfile import NamedTemporaryFile 28 | from textwrap import dedent 29 | 30 | from twisted.cred.portal import Portal 31 | from twisted.web.resource import IResource 32 | from twisted.web.guard import ( 33 | HTTPAuthSessionWrapper, 34 | BasicCredentialFactory, 35 | DigestCredentialFactory, 36 | ) 37 | from twisted.web.static import Data 38 | 39 | from twext.who.directory import DirectoryRecord 40 | 41 | from twext.who.test.test_xml import xmlService as XMLDirectoryService 42 | 43 | try: 44 | from twext.who.ldap.test.test_service import BaseTestCase as LDAPScaffold 45 | LDAPDirectoryService = LDAPScaffold.service 46 | 47 | except ImportError: 48 | LDAPDirectoryService = None 49 | 50 | try: 51 | from twext.who.opendirectory import ( 52 | DirectoryService as OpenDirectoryDirectoryService, 53 | NoQOPDigestCredentialFactory, 54 | ) 55 | except ImportError: 56 | OpenDirectoryDirectoryService = None 57 | 58 | from twext.who.checker import UsernamePasswordCredentialChecker 59 | from twext.who.checker import HTTPDigestCredentialChecker 60 | 61 | 62 | 63 | class Realm(object): 64 | @staticmethod 65 | def hello(avatarId): 66 | message = ["Hello, {0!r}!".format(avatarId)] 67 | 68 | if isinstance(avatarId, DirectoryRecord): 69 | message.append(avatarId.description().encode("utf-8")) 70 | 71 | return "\n\n".join(message) 72 | 73 | 74 | def requestAvatar(self, avatarId, mind, *interfaces): 75 | if IResource in interfaces: 76 | interface = IResource 77 | resource = Data(self.hello(avatarId), "text/plain") 78 | else: 79 | interface = None 80 | resource = None 81 | 82 | return interface, resource, lambda: None 83 | 84 | 85 | 86 | realm = Realm() 87 | 88 | rootResource = Data( 89 | data=dedent( 90 | """ 91 | 92 | 93 | Authentication tests 94 | 95 | 96 |
    97 | 98 |
  • XML Directory Service
  • 99 | 103 | 104 |
  • LDAP Directory Service
  • 105 | 109 | 110 |
  • OpenDirectory Directory Service
  • 111 | 115 | 116 |
117 | 118 | 119 | """[1:] 120 | ), 121 | type="text/html", 122 | ) 123 | 124 | 125 | def addChild(name, method, service, credentialFactory=None): 126 | if method == "Basic": 127 | checker = UsernamePasswordCredentialChecker 128 | defaultCredentialFactory = BasicCredentialFactory 129 | 130 | elif method == "Digest": 131 | checker = HTTPDigestCredentialChecker 132 | defaultCredentialFactory = ( 133 | lambda realmName: DigestCredentialFactory("md5", realmName) 134 | ) 135 | 136 | if credentialFactory is None: 137 | credentialFactory = defaultCredentialFactory 138 | 139 | resourceName = "{0}{1}".format(name, method) 140 | print "Adding resource:", resourceName 141 | 142 | rootResource.putChild( 143 | resourceName, 144 | HTTPAuthSessionWrapper( 145 | Portal(realm, [checker(service)]), 146 | [credentialFactory("{0} {1} Realm".format(name, method))] 147 | ) 148 | ) 149 | 150 | 151 | xmlFileBasic = NamedTemporaryFile(delete=True) 152 | addChild("XML", "Basic", XMLDirectoryService(xmlFileBasic.name)) 153 | 154 | xmlFileDigest = NamedTemporaryFile(delete=True) 155 | addChild("XML", "Digest", XMLDirectoryService(xmlFileDigest.name)) 156 | 157 | if LDAPDirectoryService is not None: 158 | addChild("LDAP", "Basic", LDAPDirectoryService()) 159 | addChild("LDAP", "Digest", LDAPDirectoryService()) 160 | 161 | if OpenDirectoryDirectoryService is not None: 162 | addChild("OpenDirectory", "Basic", OpenDirectoryDirectoryService()) 163 | addChild( 164 | "OpenDirectory", "Digest", OpenDirectoryDirectoryService(), 165 | credentialFactory=( 166 | lambda realmName: NoQOPDigestCredentialFactory("md5", realmName) 167 | ) 168 | ) 169 | 170 | resource = rootResource 171 | -------------------------------------------------------------------------------- /twext/who/test/test_aggregate.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | """ 18 | Aggregate directory service tests. 19 | """ 20 | 21 | from twisted.python.components import proxyForInterface 22 | from twisted.trial import unittest 23 | 24 | from ..idirectory import IDirectoryService, DirectoryConfigurationError 25 | from ..directory import DirectoryRecord 26 | from ..aggregate import DirectoryService 27 | from ..util import ConstantsContainer 28 | from . import test_directory, test_xml 29 | from .test_xml import ( 30 | BaseTest as XMLBaseTest, 31 | QueryMixIn, xmlService, TestService as XMLTestService, 32 | DirectoryServiceConvenienceTestMixIn, 33 | ) 34 | 35 | 36 | class BaseTest(XMLBaseTest): 37 | 38 | def service(self, services=None): 39 | if services is None: 40 | services = (self.xmlService(),) 41 | 42 | # 43 | # Make sure aggregate DirectoryService isn't making 44 | # implementation assumptions about the IDirectoryService 45 | # objects it gets. 46 | # 47 | services = tuple(( 48 | proxyForInterface(IDirectoryService)(s) 49 | for s in services 50 | )) 51 | 52 | class TestService(DirectoryService, QueryMixIn): 53 | pass 54 | 55 | return TestService(u"xyzzy", services) 56 | 57 | def xmlService(self, xmlData=None, serviceClass=None): 58 | return xmlService( 59 | self.mktemp(), 60 | xmlData=xmlData, 61 | serviceClass=serviceClass 62 | ) 63 | 64 | 65 | class DirectoryServiceTest( 66 | unittest.TestCase, 67 | BaseTest, 68 | DirectoryServiceConvenienceTestMixIn, 69 | test_directory.BaseDirectoryServiceTest 70 | ): 71 | serviceClass = DirectoryService 72 | directoryRecordClass = DirectoryRecord 73 | 74 | def test_repr(self): 75 | service = self.service() 76 | self.assertEquals(repr(service), "") 77 | 78 | 79 | class DirectoryServiceQueryTest( 80 | unittest.TestCase, 81 | BaseTest, 82 | test_xml.DirectoryServiceQueryTestMixIn 83 | ): 84 | pass 85 | 86 | 87 | class DirectoryServiceImmutableTest( 88 | BaseTest, 89 | test_directory.BaseDirectoryServiceImmutableTest, 90 | ): 91 | serviceClass = DirectoryService 92 | directoryRecordClass = DirectoryRecord 93 | 94 | 95 | class AggregatedBaseTest(BaseTest): 96 | 97 | def service(self): 98 | class UsersDirectoryService(XMLTestService): 99 | recordType = ConstantsContainer((XMLTestService.recordType.user,)) 100 | 101 | class GroupsDirectoryService(XMLTestService): 102 | recordType = ConstantsContainer((XMLTestService.recordType.group,)) 103 | 104 | usersService = self.xmlService( 105 | xmlData=testXMLConfigUsers, 106 | serviceClass=UsersDirectoryService 107 | ) 108 | groupsService = self.xmlService( 109 | xmlData=testXMLConfigGroups, 110 | serviceClass=GroupsDirectoryService 111 | ) 112 | 113 | return BaseTest.service( 114 | self, 115 | services=(usersService, groupsService) 116 | ) 117 | 118 | 119 | class DirectoryServiceAggregatedBaseTest( 120 | AggregatedBaseTest, 121 | DirectoryServiceTest, 122 | ): 123 | pass 124 | 125 | 126 | class DirectoryServiceAggregatedQueryTest( 127 | unittest.TestCase, 128 | AggregatedBaseTest, 129 | test_xml.DirectoryServiceQueryTestMixIn, 130 | ): 131 | pass 132 | 133 | 134 | class DirectoryServiceAggregatedImmutableTest( 135 | AggregatedBaseTest, 136 | test_directory.BaseDirectoryServiceImmutableTest, 137 | ): 138 | pass 139 | 140 | 141 | class DirectoryServiceTests(BaseTest, unittest.TestCase): 142 | 143 | def test_conflictingRecordTypes(self): 144 | self.assertRaises( 145 | DirectoryConfigurationError, 146 | self.service, 147 | services=(self.xmlService(), self.xmlService(testXMLConfigUsers)), 148 | ) 149 | 150 | 151 | testXMLConfigUsers = """ 152 | 153 | 154 | 155 | 156 | __wsanchez__ 157 | wsanchez 158 | wilfredo_sanchez 159 | Wilfredo Sanchez 160 | zehcnasw 161 | wsanchez@bitbucket.calendarserver.org 162 | wsanchez@devnull.twistedmatrix.com 163 | 164 | 165 | 166 | __glyph__ 167 | glyph 168 | Glyph Lefkowitz 169 | hpylg 170 | glyph@bitbucket.calendarserver.org 171 | glyph@devnull.twistedmatrix.com 172 | 173 | 174 | 175 | __sagen__ 176 | sagen 177 | Morgen Sagen 178 | negas 179 | sagen@bitbucket.calendarserver.org 180 | shared@example.com 181 | 182 | 183 | 184 | __cdaboo__ 185 | cdaboo 186 | Cyrus Daboo 187 | suryc 188 | cdaboo@bitbucket.calendarserver.org 189 | 190 | 191 | 192 | __dre__ 193 | A3B1158F-0564-4F5B-81E4-A89EA5FF81B0 194 | dre 195 | Andre LaBranche 196 | erd 197 | dre@bitbucket.calendarserver.org 198 | shared@example.com 199 | 200 | 201 | 202 | __exarkun__ 203 | exarkun 204 | Jean-Paul Calderone 205 | nucraxe 206 | exarkun@devnull.twistedmatrix.com 207 | 208 | 209 | 210 | __dreid__ 211 | dreid 212 | David Reid 213 | dierd 214 | dreid@devnull.twistedmatrix.com 215 | 216 | 217 | 218 | __joe__ 219 | joe 220 | Joe Schmoe 221 | eoj 222 | joe@example.com 223 | 224 | 225 | 226 | __alyssa__ 227 | alyssa 228 | Alyssa P. Hacker 229 | assyla 230 | alyssa@example.com 231 | 232 | 233 | 234 | """ 235 | 236 | 237 | testXMLConfigGroups = """ 238 | 239 | 240 | 241 | 242 | __calendar-dev__ 243 | calendar-dev 244 | Calendar Server developers 245 | dev@bitbucket.calendarserver.org 246 | __wsanchez__ 247 | __glyph__ 248 | __sagen__ 249 | __cdaboo__ 250 | __dre__ 251 | 252 | 253 | 254 | __twisted__ 255 | twisted 256 | Twisted Matrix Laboratories 257 | hack@devnull.twistedmatrix.com 258 | __wsanchez__ 259 | __glyph__ 260 | __exarkun__ 261 | __dreid__ 262 | __dre__ 263 | 264 | 265 | 266 | __developers__ 267 | developers 268 | All Developers 269 | __calendar-dev__ 270 | __twisted__ 271 | __alyssa__ 272 | 273 | 274 | 275 | """ 276 | -------------------------------------------------------------------------------- /twext/who/test/test_concurrency.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | from __future__ import print_function 18 | from twext.python.types import MappingProxyType 19 | from twext.who.directory import DirectoryRecord 20 | from twext.who.idirectory import RecordType, FieldName 21 | from twext.who.ldap._constants import LDAPAttribute, LDAPObjectClass 22 | from twext.who.ldap._service import DirectoryService as LDAPDirectoryService, \ 23 | DEFAULT_FIELDNAME_ATTRIBUTE_MAP, RecordTypeSchema 24 | from twext.who.opendirectory._service import DirectoryService as ODDirectoryService 25 | from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue 26 | from twisted.trial import unittest 27 | import time 28 | 29 | """ 30 | Test the concurrency of DS implementations against real servers. 31 | """ 32 | 33 | TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_ATTRIBUTE_MAP) 34 | TEST_FIELDNAME_MAP[FieldName.uid] = (u"uid",) 35 | 36 | TEST_RECORDTYPE_SCHEMAS_OSX = MappingProxyType({ 37 | 38 | RecordType.user: RecordTypeSchema( 39 | # cn=users 40 | relativeDN=u"cn={0}".format("users"), 41 | 42 | # (objectClass=inetOrgPerson) 43 | attributes=( 44 | ( 45 | LDAPAttribute.objectClass.value, 46 | LDAPObjectClass.inetOrgPerson.value, 47 | ), 48 | ), 49 | ), 50 | 51 | RecordType.group: RecordTypeSchema( 52 | # cn=groups 53 | relativeDN=u"cn={0}".format("groups"), 54 | 55 | # (objectClass=groupOfNames) 56 | attributes=( 57 | ( 58 | LDAPAttribute.objectClass.value, 59 | LDAPObjectClass.groupOfNames.value, 60 | ), 61 | ), 62 | ), 63 | 64 | }) 65 | 66 | TEST_RECORDTYPE_SCHEMAS_OTHER = MappingProxyType({ 67 | 68 | RecordType.user: RecordTypeSchema( 69 | # ou=person 70 | relativeDN=u"ou={0}".format("people"), 71 | 72 | # (objectClass=inetOrgPerson) 73 | attributes=( 74 | ( 75 | LDAPAttribute.objectClass.value, 76 | LDAPObjectClass.inetOrgPerson.value, 77 | ), 78 | ), 79 | ), 80 | 81 | RecordType.group: RecordTypeSchema( 82 | # ou=groupOfNames 83 | relativeDN=u"ou={0}".format(LDAPObjectClass.groupOfNames.value), 84 | 85 | # (objectClass=groupOfNames) 86 | attributes=( 87 | ( 88 | LDAPAttribute.objectClass.value, 89 | LDAPObjectClass.groupOfNames.value, 90 | ), 91 | ), 92 | ), 93 | 94 | }) 95 | 96 | 97 | class DirectoryServiceConcurrencyTest(unittest.TestCase): 98 | """ 99 | Tests for directory records. 100 | """ 101 | 102 | @inlineCallbacks 103 | def _runTest(self, num_threads, multiple_services, service_maker, details, num_requests, do_auth): 104 | 105 | if multiple_services: 106 | services = [service_maker() for _ in range(num_threads)] 107 | else: 108 | services = [service_maker()] * num_threads 109 | 110 | # Warm up each service before starting timer 111 | for n, svc in enumerate(services): 112 | record = yield svc.recordWithShortName(RecordType.user, details["user"].format(n + 1)) 113 | self.assertTrue(isinstance(record, DirectoryRecord)) 114 | 115 | start = time.time() 116 | ctr = [0] 117 | 118 | @inlineCallbacks 119 | def _records(n): 120 | for _ in range(num_requests): 121 | record = yield services[n].recordWithShortName(RecordType.user, details["user"].format(n + 1)) 122 | self.assertTrue(isinstance(record, DirectoryRecord)) 123 | ctr[0] += 1 124 | 125 | @inlineCallbacks 126 | def _auth(n): 127 | record = yield services[n].recordWithShortName(RecordType.user, details["user"].format(n + 1)) 128 | for _ in range(num_requests): 129 | result = yield record.verifyPlaintextPassword(details["pswd"].format(n + 1)) 130 | self.assertTrue(result) 131 | ctr[0] += 1 132 | 133 | dl = [] 134 | for i in range(num_threads): 135 | dl.append((_auth if do_auth else _records)(i)) 136 | 137 | dl = DeferredList(dl) 138 | yield dl 139 | 140 | returnValue((time.time() - start, ctr[0],)) 141 | 142 | @inlineCallbacks 143 | def test_ldap_multi_service(self): 144 | """ 145 | See if {ldap._service.DirectoryService is concurrent. 146 | """ 147 | 148 | num_threads = 20 149 | multiple_services = False 150 | num_requests = 100 151 | do_auth = False 152 | use_od = False 153 | configChoice = "local" 154 | 155 | configs = { 156 | "local": { 157 | "url": "ldap://localhost", 158 | "baseDN": "dc=example,dc=com", 159 | "rschema": TEST_RECORDTYPE_SCHEMAS_OSX, 160 | "user": u"user{:02d}", 161 | "pswd": u"user{:02d}", 162 | }, 163 | "example": { 164 | "url": "ldap://example.com", 165 | "baseDN": "o=example.com,o=email", 166 | "rschema": TEST_RECORDTYPE_SCHEMAS_OTHER, 167 | "user": u"TestAccount{}", 168 | "pswd": u"pswd", 169 | }, 170 | } 171 | 172 | details = configs[configChoice] 173 | 174 | def _serviceMaker(): 175 | if use_od: 176 | return ODDirectoryService( 177 | nodeName="/LDAPv3/127.0.0.1", 178 | ) 179 | else: 180 | return LDAPDirectoryService( 181 | url=details["url"], 182 | baseDN=details["baseDN"], 183 | fieldNameToAttributesMap=TEST_FIELDNAME_MAP, 184 | recordTypeSchemas=details["rschema"], 185 | threadPoolMax=20, 186 | ) 187 | 188 | duration, count = yield self._runTest(num_threads, multiple_services, _serviceMaker, details, num_requests, do_auth) 189 | 190 | print( 191 | "\n\nType: {} {} {}\nNumber of Services/Requests: {}/{}\nTime: {}\nCount: {}\n".format( 192 | "OD" if use_od else "LDAP", 193 | "Multiple" if multiple_services else "Single", 194 | "Auth" if do_auth else "query", 195 | num_threads, 196 | num_requests, 197 | duration, 198 | count, 199 | ) 200 | ) 201 | 202 | test_ldap_multi_service.skip = "Not really a unit test - requires actually server to work" 203 | -------------------------------------------------------------------------------- /twext/who/util.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twext.who.test.test_util -*- 2 | ## 3 | # Copyright (c) 2013-2017 Apple Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ## 17 | 18 | """ 19 | Directory service module utilities. 20 | """ 21 | 22 | __all__ = [ 23 | "ConstantsContainer", 24 | "uniqueResult", 25 | "describe", 26 | ] 27 | 28 | from inspect import getmembers, isclass, isfunction 29 | 30 | from twisted.python.constants import ( 31 | Names, Values, Flags, NamedConstant, ValueConstant, FlagConstant, 32 | ) 33 | 34 | from .idirectory import DirectoryServiceError 35 | 36 | 37 | class ConstantsContainer(object): 38 | """ 39 | A container for constants. 40 | """ 41 | 42 | def __init__(self, sources): 43 | self._constants = {} 44 | self._methods = {} 45 | 46 | for source in sources: 47 | if isclass(source): 48 | if issubclass(source, CONTAINER_CLASSES): 49 | self._addConstants(source.iterconstants()) 50 | self._addMethods(getmembers(source, isfunction)) 51 | else: 52 | raise TypeError( 53 | "Unknown constants type: {0}".format(source) 54 | ) 55 | 56 | elif isinstance(source, ConstantsContainer): 57 | self._addConstants(source.iterconstants()) 58 | self._addMethods(source._methods.iteritems()) 59 | 60 | elif isinstance(source, CONSTANT_CLASSES): 61 | self._addConstants((source,)) 62 | 63 | else: 64 | self._addConstants(source) 65 | 66 | def _addConstants(self, constants): 67 | for constant in constants: 68 | if hasattr(self, "_constantsClass"): 69 | if constant.__class__ != self._constantsClass: 70 | raise TypeError( 71 | "Can't mix constants classes in the " 72 | "same constants container: {0} != {1}" 73 | .format(constant.__class__, self._constantsClass) 74 | ) 75 | else: 76 | self._constantsClass = constant.__class__ 77 | 78 | if issubclass(self._constantsClass, ValueConstant): 79 | self.lookupByValue = self._lookupByValue 80 | 81 | if ( 82 | constant.name in self._constants and 83 | self._constants[constant.name] is not constant 84 | ): 85 | raise ValueError("Name conflict: {0}".format(constant.name)) 86 | 87 | self._constants[constant.name] = constant 88 | 89 | def _addMethods(self, methods): 90 | for name, method in methods: 91 | if name[0] == "_": 92 | continue 93 | 94 | if ( 95 | name in self._constants or 96 | (name in self._methods and self._methods[name] is not method) 97 | ): 98 | raise ValueError("Name conflict: {0}".format(name)) 99 | 100 | self._methods[name] = method 101 | 102 | def __getattr__(self, name): 103 | attr = self._constants.get(name, None) 104 | if attr is not None: 105 | return attr 106 | 107 | attr = self._methods.get(name, None) 108 | if attr is not None: 109 | return attr 110 | 111 | raise AttributeError(name) 112 | 113 | def iterconstants(self): 114 | return self._constants.itervalues() 115 | 116 | def lookupByName(self, name): 117 | try: 118 | return self._constants[name] 119 | except KeyError: 120 | raise ValueError(name) 121 | 122 | def _lookupByValue(self, value): 123 | for constant in self.iterconstants(): 124 | if constant.value == value: 125 | return constant 126 | raise ValueError(value) 127 | 128 | 129 | def uniqueResult(values): 130 | result = None 131 | for value in values: 132 | if result is None: 133 | result = value 134 | else: 135 | raise DirectoryServiceError( 136 | "Multiple values found where one expected." 137 | ) 138 | return result 139 | 140 | 141 | def firstResult(values): 142 | for value in values: 143 | return value 144 | return None 145 | 146 | 147 | def describe(constant): 148 | if isinstance(constant, FlagConstant): 149 | return "|".join( 150 | sorted(getattr(flag, "description", flag.name) for flag in constant) 151 | ) 152 | 153 | if isinstance(constant, (NamedConstant, ValueConstant)): 154 | for attr in ("description", "name"): 155 | description = getattr(constant, attr, None) 156 | if description is not None: 157 | return description 158 | 159 | return unicode(constant) 160 | 161 | 162 | CONTAINER_CLASSES = (Names, Values, Flags) 163 | CONSTANT_CLASSES = (NamedConstant, ValueConstant, FlagConstant) 164 | -------------------------------------------------------------------------------- /twisted/plugins/masterchild.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2010-2017 Apple Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | from zope.interface import implementer 18 | 19 | from twisted.python.reflect import namedClass 20 | from twisted.plugin import IPlugin 21 | from twisted.application.service import IServiceMaker 22 | 23 | 24 | @implementer(IPlugin, IServiceMaker) 25 | class ServiceMakerWrapper(object): 26 | """ 27 | ServiceMaker that instantiates and wraps a ServiceMaker, given a class name 28 | and arguments. 29 | """ 30 | 31 | def __init__(self, className, *args, **kwargs): 32 | """ 33 | @param className: The fully qualified name of the 34 | L{IServiceMaker}-providing class to instantiate. 35 | @type className: L{str} 36 | 37 | @param args: Sequential arguments to pass to the class's constructor. 38 | @type args: arguments L{list} 39 | 40 | @param kwargs: Keyword arguments to pass to the class's constructor. 41 | @type args: arguments L{dict} 42 | """ 43 | self.className = className 44 | self.args = args 45 | self.kwargs = kwargs 46 | 47 | @property 48 | def wrappedServiceMaker(self): 49 | if not hasattr(self, "_wrappedServiceMaker"): 50 | makerClass = namedClass(self.className) 51 | maker = makerClass(*self.args, **self.kwargs) 52 | self._wrappedServiceMaker = maker 53 | 54 | return self._wrappedServiceMaker 55 | 56 | @property 57 | def tapname(self): 58 | return self.wrappedServiceMaker.tapname 59 | 60 | @property 61 | def description(self): 62 | return self.wrappedServiceMaker.description 63 | 64 | @property 65 | def options(self): 66 | return self.wrappedServiceMaker.options 67 | 68 | def makeService(self, options): 69 | return self.wrappedServiceMaker.makeService(options) 70 | 71 | 72 | masterServiceMaker = ServiceMakerWrapper( 73 | "twext.application.masterchild.MasterServiceMaker" 74 | ) 75 | 76 | childServiceMaker = ServiceMakerWrapper( 77 | "twext.application.masterchild.ChildServiceMaker" 78 | ) 79 | --------------------------------------------------------------------------------