blob: d376c855d991afe7a8147bf870ddc0c4aba7711d [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Import hook for dev_appserver.py."""
import dummy_thread
import errno
import imp
import inspect
import itertools
import locale
import logging
import mimetypes
import os
import pickle
import random
import re
import sys
import urllib
try:
import distutils.util
except ImportError:
pass
from google.appengine import dist
from google.appengine import dist27 as dist27
from google.appengine.api import appinfo
SITE_PACKAGES = os.path.normcase(os.path.join(os.path.dirname(os.__file__),
'site-packages'))
import google.appengine
SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
google.appengine.__file__)))
CODING_COOKIE_RE = re.compile("coding[:=]\s*([-\w.]+)")
DEFAULT_ENCODING = 'ascii'
def FakeURandom(n):
"""Fake version of os.urandom."""
bytes = ''
for _ in range(n):
bytes += chr(random.randint(0, 255))
return bytes
def FakeUname():
"""Fake version of os.uname."""
return ('Linux', '', '', '', '')
def FakeUnlink(path):
"""Fake version of os.unlink."""
if os.path.isdir(path):
raise OSError(errno.ENOENT, "Is a directory", path)
else:
raise OSError(errno.EPERM, "Operation not permitted", path)
def FakeReadlink(path):
"""Fake version of os.readlink."""
raise OSError(errno.EINVAL, "Invalid argument", path)
def FakeAccess(path, mode):
"""Fake version of os.access where only reads are supported."""
if not os.path.exists(path) or mode != os.R_OK:
return False
else:
return True
def FakeSetLocale(category, value=None, original_setlocale=locale.setlocale):
"""Fake version of locale.setlocale that only supports the default."""
if value not in (None, '', 'C', 'POSIX'):
raise locale.Error('locale emulation only supports "C" locale')
return original_setlocale(category, 'C')
def FakeOpen(filename, flags, mode=0777):
"""Fake version of os.open."""
raise OSError(errno.EPERM, "Operation not permitted", filename)
def FakeRename(src, dst):
"""Fake version of os.rename."""
raise OSError(errno.EPERM, "Operation not permitted", src)
def FakeUTime(path, times):
"""Fake version of os.utime."""
raise OSError(errno.EPERM, "Operation not permitted", path)
def FakeGetPlatform():
"""Fake distutils.util.get_platform on OS/X. Pass-through otherwise."""
if sys.platform == 'darwin':
return 'macosx-'
else:
return distutils.util.get_platform()
def FakeCryptoRandomOSRNGnew(*args, **kwargs):
from Crypto.Random.OSRNG import fallback
return fallback.new(*args, **kwargs)
def NeedsMacOSXProxyFakes():
"""Returns True if the MacOS X urllib fakes should be installed."""
return (sys.platform == 'darwin' and
(2, 6, 0) <= sys.version_info < (2, 6, 4))
if NeedsMacOSXProxyFakes():
def _FakeProxyBypassHelper(fn,
original_module_dict=sys.modules.copy(),
original_uname=os.uname):
"""Setups and restores the state for the Mac OS X urllib fakes."""
def Inner(*args, **kwargs):
current_uname = os.uname
current_meta_path = sys.meta_path[:]
current_modules = sys.modules.copy()
try:
sys.modules.clear()
sys.modules.update(original_module_dict)
sys.meta_path[:] = []
os.uname = original_uname
return fn(*args, **kwargs)
finally:
sys.modules.clear()
sys.modules.update(current_modules)
os.uname = current_uname
sys.meta_path[:] = current_meta_path
return Inner
@_FakeProxyBypassHelper
def FakeProxyBypassMacOSXSysconf(
host,
original_proxy_bypass_macosx_sysconf=urllib.proxy_bypass_macosx_sysconf):
"""Fake for urllib.proxy_bypass_macosx_sysconf for Python 2.6.0 to 2.6.3."""
return original_proxy_bypass_macosx_sysconf(host)
@_FakeProxyBypassHelper
def FakeGetProxiesMacOSXSysconf(
original_getproxies_macosx_sysconf=urllib.getproxies_macosx_sysconf):
"""Fake for urllib.getproxies_macosx_sysconf for Python 2.6.0 to 2.6.3."""
return original_getproxies_macosx_sysconf()
def IsPathInSubdirectories(filename,
subdirectories,
normcase=os.path.normcase):
"""Determines if a filename is contained within one of a set of directories.
Args:
filename: Path of the file (relative or absolute).
subdirectories: Iterable collection of paths to subdirectories which the
given filename may be under.
normcase: Used for dependency injection.
Returns:
True if the supplied filename is in one of the given sub-directories or
its hierarchy of children. False otherwise.
"""
file_dir = normcase(os.path.dirname(os.path.abspath(filename)))
for parent in subdirectories:
fixed_parent = normcase(os.path.abspath(parent))
if os.path.commonprefix([file_dir, fixed_parent]) == fixed_parent:
return True
return False
def GeneratePythonPaths(*p):
"""Generate all valid filenames for the given file.
Args:
p: Positional args are the folders to the file and finally the file
without a suffix.
Returns:
A list of strings representing the given path to a file with each valid
suffix for this python build.
"""
suffixes = imp.get_suffixes()
return [os.path.join(*p) + s for s, m, t in suffixes]
class FakeFile(file):
"""File sub-class that enforces the security restrictions of the production
environment.
"""
ALLOWED_MODES = frozenset(['r', 'rb', 'U', 'rU'])
ALLOWED_FILES = set(os.path.normcase(filename)
for filename in mimetypes.knownfiles
if os.path.isfile(filename))
ALLOWED_FILES_RE = set([re.compile(r'.*/python27.zip$')])
ALLOWED_DIRS = set([
os.path.normcase(os.path.realpath(os.path.dirname(os.__file__))),
os.path.normcase(os.path.abspath(os.path.dirname(os.__file__))),
os.path.normcase(os.path.dirname(os.path.realpath(os.__file__))),
os.path.normcase(os.path.dirname(os.path.abspath(os.__file__))),
])
os_source_location = inspect.getsourcefile(os)
if os_source_location is not None:
ALLOWED_DIRS.update([
os.path.normcase(os.path.realpath(os.path.dirname(os_source_location))),
os.path.normcase(os.path.abspath(os.path.dirname(os_source_location))),
os.path.normcase(os.path.dirname(os.path.realpath(os_source_location))),
os.path.normcase(os.path.dirname(os.path.abspath(os_source_location))),
])
NOT_ALLOWED_DIRS = set([
SITE_PACKAGES,
])
ALLOWED_SITE_PACKAGE_DIRS = set(
os.path.normcase(os.path.abspath(os.path.join(SITE_PACKAGES, path)))
for path in [
])
ALLOWED_SITE_PACKAGE_FILES = set(
os.path.normcase(os.path.abspath(os.path.join(
os.path.dirname(os.__file__), 'site-packages', path)))
for path in itertools.chain(*[
[os.path.join('Crypto')],
GeneratePythonPaths('Crypto', '__init__'),
GeneratePythonPaths('Crypto', 'pct_warnings'),
[os.path.join('Crypto', 'Cipher')],
GeneratePythonPaths('Crypto', 'Cipher', '__init__'),
GeneratePythonPaths('Crypto', 'Cipher', 'blockalgo'),
GeneratePythonPaths('Crypto', 'Cipher', 'AES'),
GeneratePythonPaths('Crypto', 'Cipher', 'ARC2'),
GeneratePythonPaths('Crypto', 'Cipher', 'ARC4'),
GeneratePythonPaths('Crypto', 'Cipher', 'Blowfish'),
GeneratePythonPaths('Crypto', 'Cipher', 'CAST'),
GeneratePythonPaths('Crypto', 'Cipher', 'DES'),
GeneratePythonPaths('Crypto', 'Cipher', 'DES3'),
GeneratePythonPaths('Crypto', 'Cipher', 'PKCS1_OAEP'),
GeneratePythonPaths('Crypto', 'Cipher', 'PKCS1_v1_5'),
GeneratePythonPaths('Crypto', 'Cipher', 'XOR'),
[os.path.join('Crypto', 'Hash')],
GeneratePythonPaths('Crypto', 'Hash', '__init__'),
GeneratePythonPaths('Crypto', 'Hash', 'hashalgo'),
GeneratePythonPaths('Crypto', 'Hash', 'HMAC'),
GeneratePythonPaths('Crypto', 'Hash', 'MD2'),
GeneratePythonPaths('Crypto', 'Hash', 'MD4'),
GeneratePythonPaths('Crypto', 'Hash', 'MD5'),
GeneratePythonPaths('Crypto', 'Hash', 'SHA'),
GeneratePythonPaths('Crypto', 'Hash', 'SHA224'),
GeneratePythonPaths('Crypto', 'Hash', 'SHA256'),
GeneratePythonPaths('Crypto', 'Hash', 'SHA384'),
GeneratePythonPaths('Crypto', 'Hash', 'SHA512'),
GeneratePythonPaths('Crypto', 'Hash', 'RIPEMD'),
[os.path.join('Crypto', 'Protocol')],
GeneratePythonPaths('Crypto', 'Protocol', '__init__'),
GeneratePythonPaths('Crypto', 'Protocol', 'AllOrNothing'),
GeneratePythonPaths('Crypto', 'Protocol', 'Chaffing'),
GeneratePythonPaths('Crypto', 'Protocol', 'KDF'),
[os.path.join('Crypto', 'PublicKey')],
GeneratePythonPaths('Crypto', 'PublicKey', '__init__'),
GeneratePythonPaths('Crypto', 'PublicKey', 'DSA'),
GeneratePythonPaths('Crypto', 'PublicKey', '_DSA'),
GeneratePythonPaths('Crypto', 'PublicKey', 'ElGamal'),
GeneratePythonPaths('Crypto', 'PublicKey', 'RSA'),
GeneratePythonPaths('Crypto', 'PublicKey', '_RSA'),
GeneratePythonPaths('Crypto', 'PublicKey', 'pubkey'),
GeneratePythonPaths('Crypto', 'PublicKey', 'qNEW'),
GeneratePythonPaths('Crypto', 'PublicKey', '_slowmath'),
[os.path.join('Crypto', 'Random')],
GeneratePythonPaths('Crypto', 'Random', '__init__'),
GeneratePythonPaths('Crypto', 'Random', 'random'),
GeneratePythonPaths('Crypto', 'Random', '_UserFriendlyRNG'),
[os.path.join('Crypto', 'Random', 'OSRNG')],
GeneratePythonPaths('Crypto', 'Random', 'OSRNG', '__init__'),
GeneratePythonPaths('Crypto', 'Random', 'OSRNG', 'fallback'),
GeneratePythonPaths('Crypto', 'Random', 'OSRNG', 'nt'),
GeneratePythonPaths('Crypto', 'Random', 'OSRNG', 'posix'),
GeneratePythonPaths('Crypto', 'Random', 'OSRNG', 'rng_base'),
[os.path.join('Crypto', 'Random', 'Fortuna')],
GeneratePythonPaths('Crypto', 'Random', 'Fortuna', '__init__'),
GeneratePythonPaths('Crypto', 'Random', 'Fortuna',
'FortunaAccumulator'),
GeneratePythonPaths('Crypto', 'Random', 'Fortuna',
'FortunaGenerator'),
GeneratePythonPaths('Crypto', 'Random', 'Fortuna', 'SHAd256'),
[os.path.join('Crypto', 'Signature')],
GeneratePythonPaths('Crypto', 'Signature', '__init__'),
GeneratePythonPaths('Crypto', 'Signature', 'PKCS1_PSS'),
GeneratePythonPaths('Crypto', 'Signature', 'PKCS1_v1_5'),
[os.path.join('Crypto', 'Util')],
GeneratePythonPaths('Crypto', 'Util', '__init__'),
GeneratePythonPaths('Crypto', 'Util', 'asn1'),
GeneratePythonPaths('Crypto', 'Util', 'Counter'),
GeneratePythonPaths('Crypto', 'Util', 'RFC1751'),
GeneratePythonPaths('Crypto', 'Util', 'number'),
GeneratePythonPaths('Crypto', 'Util', '_number_new'),
GeneratePythonPaths('Crypto', 'Util', 'py3compat'),
GeneratePythonPaths('Crypto', 'Util', 'python_compat'),
GeneratePythonPaths('Crypto', 'Util', 'randpool'),
]))
_original_file = file
_root_path = None
_application_paths = None
_skip_files = None
_static_file_config_matcher = None
_allow_skipped_files = True
_availability_cache = {}
@staticmethod
def SetAllowedPaths(root_path, application_paths):
"""Configures which paths are allowed to be accessed.
Must be called at least once before any file objects are created in the
hardened environment.
Args:
root_path: Absolute path to the root of the application.
application_paths: List of additional paths that the application may
access, this must include the App Engine runtime but
not the Python library directories.
"""
FakeFile._application_paths = (set(os.path.realpath(path)
for path in application_paths) |
set(os.path.abspath(path)
for path in application_paths))
FakeFile._application_paths.add(root_path)
FakeFile._root_path = os.path.join(root_path, '')
FakeFile._availability_cache = {}
@staticmethod
def SetAllowSkippedFiles(allow_skipped_files):
"""Configures access to files matching FakeFile._skip_files.
Args:
allow_skipped_files: Boolean whether to allow access to skipped files
"""
FakeFile._allow_skipped_files = allow_skipped_files
FakeFile._availability_cache = {}
@staticmethod
def SetAllowedModule(name):
"""Allow the use of a module based on where it is located.
Meant to be used by use_library() so that it has a link back into the
trusted part of the interpreter.
Args:
name: Name of the module to allow.
"""
stream, pathname, description = imp.find_module(name)
pathname = os.path.normcase(os.path.abspath(pathname))
if stream:
stream.close()
FakeFile.ALLOWED_FILES.add(pathname)
FakeFile.ALLOWED_FILES.add(os.path.realpath(pathname))
else:
assert description[2] == imp.PKG_DIRECTORY
if pathname.startswith(SITE_PACKAGES):
FakeFile.ALLOWED_SITE_PACKAGE_DIRS.add(pathname)
FakeFile.ALLOWED_SITE_PACKAGE_DIRS.add(os.path.realpath(pathname))
else:
FakeFile.ALLOWED_DIRS.add(pathname)
FakeFile.ALLOWED_DIRS.add(os.path.realpath(pathname))
@staticmethod
def SetSkippedFiles(skip_files):
"""Sets which files in the application directory are to be ignored.
Must be called at least once before any file objects are created in the
hardened environment.
Must be called whenever the configuration was updated.
Args:
skip_files: Object with .match() method (e.g. compiled regexp).
"""
FakeFile._skip_files = skip_files
FakeFile._availability_cache = {}
@staticmethod
def SetStaticFileConfigMatcher(static_file_config_matcher):
"""Sets StaticFileConfigMatcher instance for checking if a file is static.
Must be called at least once before any file objects are created in the
hardened environment.
Must be called whenever the configuration was updated.
Args:
static_file_config_matcher: StaticFileConfigMatcher instance.
"""
FakeFile._static_file_config_matcher = static_file_config_matcher
FakeFile._availability_cache = {}
@staticmethod
def IsFileAccessible(filename, normcase=os.path.normcase,
py27_optional=False):
"""Determines if a file's path is accessible.
SetAllowedPaths(), SetSkippedFiles() and SetStaticFileConfigMatcher() must
be called before this method or else all file accesses will raise an error.
Args:
filename: Path of the file to check (relative or absolute). May be a
directory, in which case access for files inside that directory will
be checked.
normcase: Used for dependency injection.
py27_optional: Whether the filename being checked matches the name of an
optional python27 runtime library.
Returns:
True if the file is accessible, False otherwise.
"""
logical_filename = normcase(os.path.abspath(filename))
result = FakeFile._availability_cache.get(logical_filename)
if result is None:
result = FakeFile._IsFileAccessibleNoCache(logical_filename,
normcase=normcase,
py27_optional=py27_optional)
FakeFile._availability_cache[logical_filename] = result
return result
@staticmethod
def _IsFileAccessibleNoCache(logical_filename, normcase=os.path.normcase,
py27_optional=False):
"""Determines if a file's path is accessible.
This is an internal part of the IsFileAccessible implementation.
Args:
logical_filename: Absolute path of the file to check.
normcase: Used for dependency injection.
py27_optional: Whether the filename being checked matches the name of an
optional python27 runtime library.
Returns:
True if the file is accessible, False otherwise.
"""
logical_dirfakefile = logical_filename
is_dir = False
if os.path.isdir(logical_filename):
logical_dirfakefile = os.path.join(logical_filename, 'foo')
is_dir = True
if IsPathInSubdirectories(logical_dirfakefile, [FakeFile._root_path],
normcase=normcase):
relative_filename = logical_dirfakefile[len(FakeFile._root_path):]
if not FakeFile._allow_skipped_files:
path = relative_filename
if is_dir:
path = os.path.dirname(path)
while path != os.path.dirname(path):
if FakeFile._skip_files.match(path):
logging.warning('Blocking access to skipped file "%s"',
logical_filename)
return False
path = os.path.dirname(path)
if FakeFile._static_file_config_matcher.IsStaticFile(relative_filename):
logging.warning('Blocking access to static file "%s"',
logical_filename)
return False
if py27_optional:
return True
if logical_filename in FakeFile.ALLOWED_FILES:
return True
for regex in FakeFile.ALLOWED_FILES_RE:
match = regex.match(logical_filename)
if match and match.end() == len(logical_filename):
return True
if logical_filename in FakeFile.ALLOWED_SITE_PACKAGE_FILES:
return True
if IsPathInSubdirectories(logical_dirfakefile,
FakeFile.ALLOWED_SITE_PACKAGE_DIRS,
normcase=normcase):
return True
allowed_dirs = FakeFile._application_paths | FakeFile.ALLOWED_DIRS
if (IsPathInSubdirectories(logical_dirfakefile,
allowed_dirs,
normcase=normcase) and
not IsPathInSubdirectories(logical_dirfakefile,
FakeFile.NOT_ALLOWED_DIRS,
normcase=normcase)):
return True
return False
def __init__(self, filename, mode='r', bufsize=-1, **kwargs):
"""Initializer. See file built-in documentation."""
if mode not in FakeFile.ALLOWED_MODES:
raise IOError('invalid mode: %s' % mode)
if not FakeFile.IsFileAccessible(filename):
raise IOError(errno.EACCES, 'file not accessible', filename)
super(FakeFile, self).__init__(filename, mode, bufsize, **kwargs)
dist._library.SetAllowedModule = FakeFile.SetAllowedModule
class RestrictedPathFunction(object):
"""Enforces access restrictions for functions that have a file or
directory path as their first argument."""
_original_os = os
def __init__(self, original_func):
"""Initializer.
Args:
original_func: Callable that takes as its first argument the path to a
file or directory on disk; all subsequent arguments may be variable.
"""
self._original_func = original_func
def __call__(self, path, *args, **kwargs):
"""Enforces access permissions for the function passed to the constructor.
"""
if not FakeFile.IsFileAccessible(path):
raise OSError(errno.EACCES, 'path not accessible', path)
return self._original_func(path, *args, **kwargs)
def GetSubmoduleName(fullname):
"""Determines the leaf submodule name of a full module name.
Args:
fullname: Fully qualified module name, e.g. 'foo.bar.baz'
Returns:
Submodule name, e.g. 'baz'. If the supplied module has no submodule (e.g.,
'stuff'), the returned value will just be that module name ('stuff').
"""
return fullname.rsplit('.', 1)[-1]
class CouldNotFindModuleError(ImportError):
"""Raised when a module could not be found.
In contrast to when a module has been found, but cannot be loaded because of
hardening restrictions.
"""
class Py27OptionalModuleError(ImportError):
"""Raised for error conditions relating to optional Python 2.7 modules."""
def Trace(func):
"""Call stack logging decorator for HardenedModulesHook class.
This decorator logs the call stack of the HardenedModulesHook class as
it executes, indenting logging messages based on the current stack depth.
Args:
func: the function to decorate.
Returns:
The decorated function.
"""
def Decorate(self, *args, **kwargs):
args_to_show = []
if args is not None:
args_to_show.extend(str(argument) for argument in args)
if kwargs is not None:
args_to_show.extend('%s=%s' % (key, value)
for key, value in kwargs.iteritems())
args_string = ', '.join(args_to_show)
self.log('Entering %s(%s)', func.func_name, args_string)
self._indent_level += 1
try:
return func(self, *args, **kwargs)
finally:
self._indent_level -= 1
self.log('Exiting %s(%s)', func.func_name, args_string)
return Decorate
class HardenedModulesHook(object):
"""Meta import hook that restricts the modules used by applications to match
the production environment.
Module controls supported:
- Disallow native/extension modules from being loaded
- Disallow built-in and/or Python-distributed modules from being loaded
- Replace modules with completely empty modules
- Override specific module attributes
- Replace one module with another
After creation, this object should be added to the front of the sys.meta_path
list (which may need to be created). The sys.path_importer_cache dictionary
should also be cleared, to prevent loading any non-restricted modules.
See PEP302 for more info on how this works:
http://www.python.org/dev/peps/pep-0302/
"""
ENABLE_LOGGING = False
def log(self, message, *args):
"""Logs an import-related message to stderr, with indentation based on
current call-stack depth.
Args:
message: Logging format string.
args: Positional format parameters for the logging message.
"""
if HardenedModulesHook.ENABLE_LOGGING:
indent = self._indent_level * ' '
print >>sys.__stderr__, indent + (message % args)
_WHITE_LIST_C_MODULES = [
'py_streamhtmlparser',
'AES',
'ARC2',
'ARC4',
'Blowfish',
'CAST',
'DES',
'DES3',
'MD2',
'MD4',
'RIPEMD',
'RIPEMD160',
'SHA256',
'XOR',
'_AES',
'_ARC2',
'_ARC4',
'_Blowfish',
'_CAST',
'_DES',
'_DES3',
'_MD2',
'_MD4',
'_RIPEMD160',
'_SHA224',
'_SHA256',
'_SHA384',
'_SHA512',
'_XOR',
'_Crypto_Cipher__AES',
'_Crypto_Cipher__ARC2',
'_Crypto_Cipher__ARC4',
'_Crypto_Cipher__Blowfish',
'_Crypto_Cipher__CAST',
'_Crypto_Cipher__DES',
'_Crypto_Cipher__DES3',
'_Crypto_Cipher__XOR',
'_Crypto_Hash__MD2',
'_Crypto_Hash__MD4',
'_Crypto_Hash__RIPEMD',
'_Crypto_Hash__SHA256',
'array',
'binascii',
'bz2',
'cmath',
'collections',
'crypt',
'cStringIO',
'datetime',
'errno',
'exceptions',
'gc',
'itertools',
'math',
'md5',
'operator',
'posix',
'posixpath',
'pyexpat',
'sha',
'struct',
'strxor',
'sys',
'time',
'timing',
'unicodedata',
'zlib',
'_ast',
'_bisect',
'_codecs',
'_codecs_cn',
'_codecs_hk',
'_codecs_iso2022',
'_codecs_jp',
'_codecs_kr',
'_codecs_tw',
'_collections',
'_counter',
'_csv',
'_elementtree',
'_fastmath',
'_functools',
'_hashlib',
'_heapq',
'_io',
'_locale',
'_lsprof',
'_md5',
'_multibytecodec',
'_scproxy',
'_random',
'_sha',
'_sha256',
'_sha512',
'_sre',
'_struct',
'_types',
'_weakref',
'__main__',
]
_PY27_ALLOWED_MODULES = [
'_bytesio',
'_fileio',
'_json',
'_symtable',
'_yaml',
'parser',
'strop',
]
__PY27_OPTIONAL_ALLOWED_MODULES = {
'django': [],
'jinja2': ['_debugsupport', '_speedups'],
'lxml': ['etree', 'objectify'],
'markupsafe': ['_speedups'],
'matplotlib': [
'ft2font',
'ttconv',
'_png',
'_backend_agg',
'_path',
'_image',
'_cntr',
'nxutils',
'_delaunay',
'_tri',
],
'numpy': [
'_capi',
'_compiled_base',
'_dotblas',
'fftpack_lite',
'lapack_lite',
'mtrand',
'multiarray',
'scalarmath',
'_sort',
'umath',
'umath_tests',
],
'PIL': ['_imaging', '_imagingcms', '_imagingft', '_imagingmath'],
'setuptools': [],
}
__CRYPTO_CIPHER_ALLOWED_MODULES = [
'MODE_CBC',
'MODE_CFB',
'MODE_CTR',
'MODE_ECB',
'MODE_OFB',
'block_size',
'key_size',
'new',
]
_WHITE_LIST_PARTIAL_MODULES = {
'Crypto.Cipher.AES': __CRYPTO_CIPHER_ALLOWED_MODULES,
'Crypto.Cipher.ARC2': __CRYPTO_CIPHER_ALLOWED_MODULES,
'Crypto.Cipher.Blowfish': __CRYPTO_CIPHER_ALLOWED_MODULES,
'Crypto.Cipher.CAST': __CRYPTO_CIPHER_ALLOWED_MODULES,
'Crypto.Cipher.DES': __CRYPTO_CIPHER_ALLOWED_MODULES,
'Crypto.Cipher.DES3': __CRYPTO_CIPHER_ALLOWED_MODULES,
'gc': [
'enable',
'disable',
'isenabled',
'collect',
'get_debug',
'set_threshold',
'get_threshold',
'get_count'
],
'os': [
'access',
'altsep',
'curdir',
'defpath',
'devnull',
'environ',
'error',
'extsep',
'EX_NOHOST',
'EX_NOINPUT',
'EX_NOPERM',
'EX_NOUSER',
'EX_OK',
'EX_OSERR',
'EX_OSFILE',
'EX_PROTOCOL',
'EX_SOFTWARE',
'EX_TEMPFAIL',
'EX_UNAVAILABLE',
'EX_USAGE',
'F_OK',
'getcwd',
'getcwdu',
'getenv',
'listdir',
'lstat',
'name',
'NGROUPS_MAX',
'O_APPEND',
'O_CREAT',
'O_DIRECT',
'O_DIRECTORY',
'O_DSYNC',
'O_EXCL',
'O_LARGEFILE',
'O_NDELAY',
'O_NOCTTY',
'O_NOFOLLOW',
'O_NONBLOCK',
'O_RDONLY',
'O_RDWR',
'O_RSYNC',
'O_SYNC',
'O_TRUNC',
'O_WRONLY',
'open',
'pardir',
'path',
'pathsep',
'R_OK',
'readlink',
'remove',
'rename',
'SEEK_CUR',
'SEEK_END',
'SEEK_SET',
'sep',
'stat',
'stat_float_times',
'stat_result',
'strerror',
'TMP_MAX',
'unlink',
'urandom',
'utime',
'walk',
'WCOREDUMP',
'WEXITSTATUS',
'WIFEXITED',
'WIFSIGNALED',
'WIFSTOPPED',
'WNOHANG',
'WSTOPSIG',
'WTERMSIG',
'WUNTRACED',
'W_OK',
'X_OK',
'_get_exports_list',
],
'signal': [
],
'ssl': [
],
}
_MODULE_OVERRIDES = {
'locale': {
'setlocale': FakeSetLocale,
},
'os': {
'access': FakeAccess,
'listdir': RestrictedPathFunction(os.listdir),
'lstat': RestrictedPathFunction(os.stat),
'open': FakeOpen,
'readlink': FakeReadlink,
'remove': FakeUnlink,
'rename': FakeRename,
'stat': RestrictedPathFunction(os.stat),
'uname': FakeUname,
'unlink': FakeUnlink,
'urandom': FakeURandom,
'utime': FakeUTime,
},
'signal': {
'__doc__': None,
},
'distutils.util': {
'get_platform': FakeGetPlatform,
},
'Crypto.Random.OSRNG': {
'new': FakeCryptoRandomOSRNGnew,
},
}
_ENABLED_FILE_TYPES = (
imp.PKG_DIRECTORY,
imp.PY_SOURCE,
imp.PY_COMPILED,
imp.C_BUILTIN,
)
def __init__(self,
config,
module_dict,
app_code_path,
imp_module=imp,
os_module=os,
dummy_thread_module=dummy_thread,
pickle_module=pickle):
"""Initializer.
Args:
config: AppInfoExternal instance representing the parsed app.yaml file.
module_dict: Module dictionary to use for managing system modules.
Should be sys.modules.
app_code_path: The absolute path to the application code on disk.
imp_module, os_module, dummy_thread_module, etc.: References to
modules that exist in the dev_appserver that must be used by this class
in order to function, even if these modules have been unloaded from
sys.modules.
"""
self._config = config
self._module_dict = module_dict
self._imp = imp_module
self._os = os_module
self._dummy_thread = dummy_thread_module
self._pickle = pickle
self._indent_level = 0
self._app_code_path = app_code_path
self._white_list_c_modules = list(self._WHITE_LIST_C_MODULES)
self._white_list_partial_modules = dict(self._WHITE_LIST_PARTIAL_MODULES)
self._enabled_modules = []
if self._config and self._config.runtime == 'python27':
self._white_list_c_modules.extend(self._PY27_ALLOWED_MODULES)
self._white_list_partial_modules['os'] = (
list(self._white_list_partial_modules['os']) +
['getpid', 'getuid', 'sys'])
for k in self._white_list_partial_modules.keys():
if k.startswith('Crypto'):
del self._white_list_partial_modules[k]
webob_path = os.path.join(SDK_ROOT, 'lib', 'webob-1.1.1')
if webob_path not in sys.path:
sys.path.insert(1, webob_path)
for libentry in self._config.GetAllLibraries():
self._enabled_modules.append(libentry.name)
extra = self.__PY27_OPTIONAL_ALLOWED_MODULES.get(libentry.name)
logging.debug('Enabling %s: %r', libentry.name, extra)
if extra:
self._white_list_c_modules.extend(extra)
if libentry.name == 'django':
if 'django' not in self._module_dict:
version = libentry.version
if version == 'latest':
django_library = appinfo._NAME_TO_SUPPORTED_LIBRARY['django']
version = django_library.non_deprecated_versions[-1]
if google.__name__.endswith('3'):
try:
__import__('django.v' + version.replace('.', '_'))
continue
except ImportError:
sys.modules.pop('django', None)
sitedir = os.path.join(SDK_ROOT,
'lib',
'django-%s' % version)
if os.path.isdir(sitedir):
logging.debug('Enabling Django version %s at %s',
version, sitedir)
sys.path[:] = [dirname
for dirname in sys.path
if not dirname.startswith(os.path.join(
SDK_ROOT, 'lib', 'django'))]
sys.path.insert(1, sitedir)
else:
logging.warn('Enabling Django version %s (no directory found)',
version)
@Trace
def find_module(self, fullname, path=None):
"""See PEP 302."""
if fullname in ('cPickle', 'thread'):
return self
search_path = path
all_modules = fullname.split('.')
try:
for index, current_module in enumerate(all_modules):
current_module_fullname = '.'.join(all_modules[:index + 1])
if (current_module_fullname == fullname and not
self.StubModuleExists(fullname)):
self.FindModuleRestricted(current_module,
current_module_fullname,
search_path)
else:
if current_module_fullname in self._module_dict:
module = self._module_dict[current_module_fullname]
else:
module = self.FindAndLoadModule(current_module,
current_module_fullname,
search_path)
if hasattr(module, '__path__'):
search_path = module.__path__
except CouldNotFindModuleError:
return None
except Py27OptionalModuleError, err:
logging.error(err)
raise
return self
def StubModuleExists(self, name):
"""Check if the named module has a stub replacement."""
if name in sys.builtin_module_names:
name = 'py_%s' % name
if self._config and self._config.runtime == 'python27':
if name in dist27.MODULE_OVERRIDES:
return True
else:
if name in dist.__all__:
return True
return False
def ImportStubModule(self, name):
"""Import the stub module replacement for the specified module."""
if name in sys.builtin_module_names:
name = 'py_%s' % name
providing_dist = dist
if self._config and self._config.runtime == 'python27':
if name in dist27.__all__:
providing_dist = dist27
fullname = '%s.%s' % (providing_dist.__name__, name)
__import__(fullname, {}, {})
return sys.modules[fullname]
@Trace
def FixModule(self, module):
"""Prunes and overrides restricted module attributes.
Args:
module: The module to prune. This should be a new module whose attributes
reference back to the real module's __dict__ members.
"""
if module.__name__ in self._white_list_partial_modules:
allowed_symbols = self._white_list_partial_modules[module.__name__]
for symbol in set(module.__dict__) - set(allowed_symbols):
if not (symbol.startswith('__') and symbol.endswith('__')):
del module.__dict__[symbol]
if module.__name__ in self._MODULE_OVERRIDES:
module.__dict__.update(self._MODULE_OVERRIDES[module.__name__])
if module.__name__ == 'urllib' and NeedsMacOSXProxyFakes():
module.__dict__.update(
{'proxy_bypass_macosx_sysconf': FakeProxyBypassMacOSXSysconf,
'getproxies_macosx_sysconf': FakeGetProxiesMacOSXSysconf})
@Trace
def FindModuleRestricted(self,
submodule,
submodule_fullname,
search_path):
"""Locates a module while enforcing module import restrictions.
Args:
submodule: The short name of the submodule (i.e., the last section of
the fullname; for 'foo.bar' this would be 'bar').
submodule_fullname: The fully qualified name of the module to find (e.g.,
'foo.bar').
search_path: List of paths to search for to find this module. Should be
None if the current sys.path should be used.
Returns:
Tuple (source_file, pathname, description) where:
source_file: File-like object that contains the module; in the case
of packages, this will be None, which implies to look at __init__.py.
pathname: String containing the full path of the module on disk.
description: Tuple returned by imp.find_module().
However, in the case of an import using a path hook (e.g. a zipfile),
source_file will be a PEP-302-style loader object, pathname will be None,
and description will be a tuple filled with None values.
Raises:
ImportError exception if the requested module was found, but importing
it is disallowed.
CouldNotFindModuleError exception if the request module could not even
be found for import.
"""
if search_path is None:
search_path = [None] + sys.path
py27_optional = False
py27_enabled = False
topmodule = None
if self._config and self._config.runtime == 'python27':
topmodule = submodule_fullname.split('.')[0]
if topmodule in self.__PY27_OPTIONAL_ALLOWED_MODULES:
py27_optional = True
py27_enabled = topmodule in self._enabled_modules
elif topmodule == 'Crypto':
py27_optional = True
py27_enabled = 'pycrypto' in self._enabled_modules
import_error = None
for path_entry in search_path:
result = self.FindPathHook(submodule, submodule_fullname, path_entry)
if result is not None:
source_file, pathname, description = result
if description == (None, None, None):
return result
suffix, mode, file_type = description
try:
if (file_type not in (self._imp.C_BUILTIN, self._imp.C_EXTENSION)):
pkg_pathname = pathname
if file_type == self._imp.PKG_DIRECTORY:
pkg_pathname = os.path.join(pkg_pathname, '__init__.py')
if not FakeFile.IsFileAccessible(
pkg_pathname, py27_optional=py27_optional):
error_message = 'Access to module file denied: %s' % pathname
logging.debug(error_message)
raise ImportError(error_message)
if (file_type not in self._ENABLED_FILE_TYPES and
submodule not in self._white_list_c_modules):
error_message = ('Could not import "%s": Disallowed C-extension '
'or built-in module' % submodule_fullname)
logging.debug(error_message)
raise ImportError(error_message)
if (py27_optional and not py27_enabled and
not pathname.startswith(self._app_code_path)):
error_message = ('Third party package %s not enabled.' % topmodule)
logging.debug(error_message)
raise ImportError(error_message)
return source_file, pathname, description
except ImportError, e:
import_error = e
if py27_optional and submodule_fullname == topmodule:
if py27_enabled:
msg = ('Third party package %s was enabled in app.yaml '
'but not found on import. You may have to download '
'and install it.' % topmodule)
else:
msg = ('Third party package %s must be included in the '
'"libraries:" clause of your app.yaml file '
'in order to be imported.' % topmodule)
logging.debug(msg)
raise Py27OptionalModuleError(msg)
if import_error:
raise import_error
self.log('Could not find module "%s"', submodule_fullname)
raise CouldNotFindModuleError()
def FindPathHook(self, submodule, submodule_fullname, path_entry):
"""Helper for FindModuleRestricted to find a module in a sys.path entry.
Args:
submodule:
submodule_fullname:
path_entry: A single sys.path entry, or None representing the builtins.
Returns:
Either None (if nothing was found), or a triple (source_file, path_name,
description). See the doc string for FindModuleRestricted() for the
meaning of the latter.
"""
if path_entry is None:
if submodule_fullname in sys.builtin_module_names:
try:
result = self._imp.find_module(submodule)
except ImportError:
pass
else:
source_file, pathname, description = result
suffix, mode, file_type = description
if file_type == self._imp.C_BUILTIN:
return result
return None
if path_entry in sys.path_importer_cache:
importer = sys.path_importer_cache[path_entry]
else:
importer = None
for hook in sys.path_hooks:
try:
importer = hook(path_entry)
break
except ImportError:
pass
sys.path_importer_cache[path_entry] = importer
if importer is None:
try:
return self._imp.find_module(submodule, [path_entry])
except ImportError:
pass
else:
loader = importer.find_module(submodule_fullname)
if loader is not None:
return (loader, None, (None, None, None))
return None
@Trace
def LoadModuleRestricted(self,
submodule_fullname,
source_file,
pathname,
description):
"""Loads a module while enforcing module import restrictions.
As a byproduct, the new module will be added to the module dictionary.
Args:
submodule_fullname: The fully qualified name of the module to find (e.g.,
'foo.bar').
source_file: File-like object that contains the module's source code,
or a PEP-302-style loader object.
pathname: String containing the full path of the module on disk.
description: Tuple returned by imp.find_module(), or (None, None, None)
in case source_file is a PEP-302-style loader object.
Returns:
The new module.
Raises:
ImportError exception of the specified module could not be loaded for
whatever reason.
"""
if description == (None, None, None):
return source_file.load_module(submodule_fullname)
try:
try:
return self._imp.load_module(submodule_fullname,
source_file,
pathname,
description)
except:
if submodule_fullname in self._module_dict:
del self._module_dict[submodule_fullname]
raise
finally:
if source_file is not None:
source_file.close()
@Trace
def FindAndLoadModule(self,
submodule,
submodule_fullname,
search_path):
"""Finds and loads a module, loads it, and adds it to the module dictionary.
Args:
submodule: Name of the module to import (e.g., baz).
submodule_fullname: Full name of the module to import (e.g., foo.bar.baz).
search_path: Path to use for searching for this submodule. For top-level
modules this should be None; otherwise it should be the __path__
attribute from the parent package.
Returns:
A new module instance that has been inserted into the module dictionary
supplied to __init__.
Raises:
ImportError exception if the module could not be loaded for whatever
reason (e.g., missing, not allowed).
"""
module = self._imp.new_module(submodule_fullname)
if submodule_fullname == 'thread':
module.__dict__.update(self._dummy_thread.__dict__)
module.__name__ = 'thread'
elif submodule_fullname == 'cPickle':
module.__dict__.update(self._pickle.__dict__)
module.__name__ = 'cPickle'
elif submodule_fullname == 'os':
module.__dict__.update(self._os.__dict__)
elif submodule_fullname == 'ssl':
pass
elif self.StubModuleExists(submodule_fullname):
module = self.ImportStubModule(submodule_fullname)
else:
source_file, pathname, description = self.FindModuleRestricted(submodule, submodule_fullname, search_path)
module = self.LoadModuleRestricted(submodule_fullname,
source_file,
pathname,
description)
if (getattr(module, '__path__', None) is not None and
search_path != self._app_code_path):
try:
app_search_path = os.path.join(self._app_code_path,
*(submodule_fullname.split('.')[:-1]))
source_file, pathname, description = self.FindModuleRestricted(submodule,
submodule_fullname,
[app_search_path])
module.__path__.append(pathname)
except ImportError, e:
pass
module.__loader__ = self
self.FixModule(module)
if submodule_fullname not in self._module_dict:
self._module_dict[submodule_fullname] = module
if submodule_fullname != submodule:
parent_module = self._module_dict.get(
submodule_fullname[:-len(submodule) - 1])
if parent_module and not hasattr(parent_module, submodule):
setattr(parent_module, submodule, module)
if submodule_fullname == 'os':
os_path_name = module.path.__name__
os_path = self.FindAndLoadModule(os_path_name, os_path_name, search_path)
self._module_dict['os.path'] = os_path
module.__dict__['path'] = os_path
return module
@Trace
def GetParentPackage(self, fullname):
"""Retrieves the parent package of a fully qualified module name.
Args:
fullname: Full name of the module whose parent should be retrieved (e.g.,
foo.bar).
Returns:
Module instance for the parent or None if there is no parent module.
Raise:
ImportError exception if the module's parent could not be found.
"""
all_modules = fullname.split('.')
parent_module_fullname = '.'.join(all_modules[:-1])
if parent_module_fullname:
if self.find_module(fullname) is None:
raise ImportError('Could not find module %s' % fullname)
return self._module_dict[parent_module_fullname]
return None
@Trace
def GetParentSearchPath(self, fullname):
"""Determines the search path of a module's parent package.
Args:
fullname: Full name of the module to look up (e.g., foo.bar).
Returns:
Tuple (submodule, search_path) where:
submodule: The last portion of the module name from fullname (e.g.,
if fullname is foo.bar, then this is bar).
search_path: List of paths that belong to the parent package's search
path or None if there is no parent package.
Raises:
ImportError exception if the module or its parent could not be found.
"""
submodule = GetSubmoduleName(fullname)
parent_package = self.GetParentPackage(fullname)
search_path = None
if parent_package is not None and hasattr(parent_package, '__path__'):
search_path = parent_package.__path__
return submodule, search_path
@Trace
def GetModuleInfo(self, fullname):
"""Determines the path on disk and the search path of a module or package.
Args:
fullname: Full name of the module to look up (e.g., foo.bar).
Returns:
Tuple (pathname, search_path, submodule) where:
pathname: String containing the full path of the module on disk,
or None if the module wasn't loaded from disk (e.g. from a zipfile).
search_path: List of paths that belong to the found package's search
path or None if found module is not a package.
submodule: The relative name of the submodule that's being imported.
"""
submodule, search_path = self.GetParentSearchPath(fullname)
source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path)
suffix, mode, file_type = description
module_search_path = None
if file_type == self._imp.PKG_DIRECTORY:
module_search_path = [pathname]
pathname = os.path.join(pathname, '__init__%spy' % os.extsep)
return pathname, module_search_path, submodule
@Trace
def load_module(self, fullname):
"""See PEP 302."""
all_modules = fullname.split('.')
submodule = all_modules[-1]
parent_module_fullname = '.'.join(all_modules[:-1])
search_path = None
if parent_module_fullname and parent_module_fullname in self._module_dict:
parent_module = self._module_dict[parent_module_fullname]
if hasattr(parent_module, '__path__'):
search_path = parent_module.__path__
return self.FindAndLoadModule(submodule, fullname, search_path)
@Trace
def is_package(self, fullname):
"""See PEP 302 extensions."""
submodule, search_path = self.GetParentSearchPath(fullname)
source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path)
suffix, mode, file_type = description
if file_type == self._imp.PKG_DIRECTORY:
return True
return False
@Trace
def get_source(self, fullname):
"""See PEP 302 extensions."""
full_path, search_path, submodule = self.GetModuleInfo(fullname)
if full_path is None:
return None
source_file = open(full_path)
try:
return source_file.read()
finally:
source_file.close()
@Trace
def get_code(self, fullname):
"""See PEP 302 extensions."""
full_path, search_path, submodule = self.GetModuleInfo(fullname)
if full_path is None:
return None
source_file = open(full_path)
try:
source_code = source_file.read()
finally:
source_file.close()
source_code = source_code.replace('\r\n', '\n')
if not source_code.endswith('\n'):
source_code += '\n'
encoding = DEFAULT_ENCODING
for line in source_code.split('\n', 2)[:2]:
matches = CODING_COOKIE_RE.findall(line)
if matches:
encoding = matches[0].lower()
source_code.decode(encoding)
return compile(source_code, full_path, 'exec')