OLD | NEW |
(Empty) | |
| 1 import os |
| 2 import sys |
| 3 import tempfile |
| 4 import operator |
| 5 import functools |
| 6 import itertools |
| 7 import re |
| 8 |
| 9 import pkg_resources |
| 10 |
| 11 if os.name == "java": |
| 12 import org.python.modules.posix.PosixModule as _os |
| 13 else: |
| 14 _os = sys.modules[os.name] |
| 15 try: |
| 16 _file = file |
| 17 except NameError: |
| 18 _file = None |
| 19 _open = open |
| 20 from distutils.errors import DistutilsError |
| 21 from pkg_resources import working_set |
| 22 |
| 23 from setuptools.compat import builtins |
| 24 |
| 25 __all__ = [ |
| 26 "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup", |
| 27 ] |
| 28 |
| 29 def _execfile(filename, globals, locals=None): |
| 30 """ |
| 31 Python 3 implementation of execfile. |
| 32 """ |
| 33 mode = 'rb' |
| 34 # Python 2.6 compile requires LF for newlines, so use deprecated |
| 35 # Universal newlines support. |
| 36 if sys.version_info < (2, 7): |
| 37 mode += 'U' |
| 38 with open(filename, mode) as stream: |
| 39 script = stream.read() |
| 40 if locals is None: |
| 41 locals = globals |
| 42 code = compile(script, filename, 'exec') |
| 43 exec(code, globals, locals) |
| 44 |
| 45 def run_setup(setup_script, args): |
| 46 """Run a distutils setup script, sandboxed in its directory""" |
| 47 old_dir = os.getcwd() |
| 48 save_argv = sys.argv[:] |
| 49 save_path = sys.path[:] |
| 50 setup_dir = os.path.abspath(os.path.dirname(setup_script)) |
| 51 temp_dir = os.path.join(setup_dir,'temp') |
| 52 if not os.path.isdir(temp_dir): os.makedirs(temp_dir) |
| 53 save_tmp = tempfile.tempdir |
| 54 save_modules = sys.modules.copy() |
| 55 pr_state = pkg_resources.__getstate__() |
| 56 try: |
| 57 tempfile.tempdir = temp_dir |
| 58 os.chdir(setup_dir) |
| 59 try: |
| 60 sys.argv[:] = [setup_script]+list(args) |
| 61 sys.path.insert(0, setup_dir) |
| 62 # reset to include setup dir, w/clean callback list |
| 63 working_set.__init__() |
| 64 working_set.callbacks.append(lambda dist:dist.activate()) |
| 65 def runner(): |
| 66 ns = dict(__file__=setup_script, __name__='__main__') |
| 67 _execfile(setup_script, ns) |
| 68 DirectorySandbox(setup_dir).run(runner) |
| 69 except SystemExit: |
| 70 v = sys.exc_info()[1] |
| 71 if v.args and v.args[0]: |
| 72 raise |
| 73 # Normal exit, just return |
| 74 finally: |
| 75 pkg_resources.__setstate__(pr_state) |
| 76 sys.modules.update(save_modules) |
| 77 # remove any modules imported within the sandbox |
| 78 del_modules = [ |
| 79 mod_name for mod_name in sys.modules |
| 80 if mod_name not in save_modules |
| 81 # exclude any encodings modules. See #285 |
| 82 and not mod_name.startswith('encodings.') |
| 83 ] |
| 84 list(map(sys.modules.__delitem__, del_modules)) |
| 85 os.chdir(old_dir) |
| 86 sys.path[:] = save_path |
| 87 sys.argv[:] = save_argv |
| 88 tempfile.tempdir = save_tmp |
| 89 |
| 90 |
| 91 class AbstractSandbox: |
| 92 """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts""" |
| 93 |
| 94 _active = False |
| 95 |
| 96 def __init__(self): |
| 97 self._attrs = [ |
| 98 name for name in dir(_os) |
| 99 if not name.startswith('_') and hasattr(self,name) |
| 100 ] |
| 101 |
| 102 def _copy(self, source): |
| 103 for name in self._attrs: |
| 104 setattr(os, name, getattr(source,name)) |
| 105 |
| 106 def run(self, func): |
| 107 """Run 'func' under os sandboxing""" |
| 108 try: |
| 109 self._copy(self) |
| 110 if _file: |
| 111 builtins.file = self._file |
| 112 builtins.open = self._open |
| 113 self._active = True |
| 114 return func() |
| 115 finally: |
| 116 self._active = False |
| 117 if _file: |
| 118 builtins.file = _file |
| 119 builtins.open = _open |
| 120 self._copy(_os) |
| 121 |
| 122 def _mk_dual_path_wrapper(name): |
| 123 original = getattr(_os,name) |
| 124 def wrap(self,src,dst,*args,**kw): |
| 125 if self._active: |
| 126 src,dst = self._remap_pair(name,src,dst,*args,**kw) |
| 127 return original(src,dst,*args,**kw) |
| 128 return wrap |
| 129 |
| 130 for name in ["rename", "link", "symlink"]: |
| 131 if hasattr(_os,name): locals()[name] = _mk_dual_path_wrapper(name) |
| 132 |
| 133 def _mk_single_path_wrapper(name, original=None): |
| 134 original = original or getattr(_os,name) |
| 135 def wrap(self,path,*args,**kw): |
| 136 if self._active: |
| 137 path = self._remap_input(name,path,*args,**kw) |
| 138 return original(path,*args,**kw) |
| 139 return wrap |
| 140 |
| 141 if _file: |
| 142 _file = _mk_single_path_wrapper('file', _file) |
| 143 _open = _mk_single_path_wrapper('open', _open) |
| 144 for name in [ |
| 145 "stat", "listdir", "chdir", "open", "chmod", "chown", "mkdir", |
| 146 "remove", "unlink", "rmdir", "utime", "lchown", "chroot", "lstat", |
| 147 "startfile", "mkfifo", "mknod", "pathconf", "access" |
| 148 ]: |
| 149 if hasattr(_os,name): locals()[name] = _mk_single_path_wrapper(name) |
| 150 |
| 151 def _mk_single_with_return(name): |
| 152 original = getattr(_os,name) |
| 153 def wrap(self,path,*args,**kw): |
| 154 if self._active: |
| 155 path = self._remap_input(name,path,*args,**kw) |
| 156 return self._remap_output(name, original(path,*args,**kw)) |
| 157 return original(path,*args,**kw) |
| 158 return wrap |
| 159 |
| 160 for name in ['readlink', 'tempnam']: |
| 161 if hasattr(_os,name): locals()[name] = _mk_single_with_return(name) |
| 162 |
| 163 def _mk_query(name): |
| 164 original = getattr(_os,name) |
| 165 def wrap(self,*args,**kw): |
| 166 retval = original(*args,**kw) |
| 167 if self._active: |
| 168 return self._remap_output(name, retval) |
| 169 return retval |
| 170 return wrap |
| 171 |
| 172 for name in ['getcwd', 'tmpnam']: |
| 173 if hasattr(_os,name): locals()[name] = _mk_query(name) |
| 174 |
| 175 def _validate_path(self,path): |
| 176 """Called to remap or validate any path, whether input or output""" |
| 177 return path |
| 178 |
| 179 def _remap_input(self,operation,path,*args,**kw): |
| 180 """Called for path inputs""" |
| 181 return self._validate_path(path) |
| 182 |
| 183 def _remap_output(self,operation,path): |
| 184 """Called for path outputs""" |
| 185 return self._validate_path(path) |
| 186 |
| 187 def _remap_pair(self,operation,src,dst,*args,**kw): |
| 188 """Called for path pairs like rename, link, and symlink operations""" |
| 189 return ( |
| 190 self._remap_input(operation+'-from',src,*args,**kw), |
| 191 self._remap_input(operation+'-to',dst,*args,**kw) |
| 192 ) |
| 193 |
| 194 |
| 195 if hasattr(os, 'devnull'): |
| 196 _EXCEPTIONS = [os.devnull,] |
| 197 else: |
| 198 _EXCEPTIONS = [] |
| 199 |
| 200 try: |
| 201 from win32com.client.gencache import GetGeneratePath |
| 202 _EXCEPTIONS.append(GetGeneratePath()) |
| 203 del GetGeneratePath |
| 204 except ImportError: |
| 205 # it appears pywin32 is not installed, so no need to exclude. |
| 206 pass |
| 207 |
| 208 class DirectorySandbox(AbstractSandbox): |
| 209 """Restrict operations to a single subdirectory - pseudo-chroot""" |
| 210 |
| 211 write_ops = dict.fromkeys([ |
| 212 "open", "chmod", "chown", "mkdir", "remove", "unlink", "rmdir", |
| 213 "utime", "lchown", "chroot", "mkfifo", "mknod", "tempnam", |
| 214 ]) |
| 215 |
| 216 _exception_patterns = [ |
| 217 # Allow lib2to3 to attempt to save a pickled grammar object (#121) |
| 218 '.*lib2to3.*\.pickle$', |
| 219 ] |
| 220 "exempt writing to paths that match the pattern" |
| 221 |
| 222 def __init__(self, sandbox, exceptions=_EXCEPTIONS): |
| 223 self._sandbox = os.path.normcase(os.path.realpath(sandbox)) |
| 224 self._prefix = os.path.join(self._sandbox,'') |
| 225 self._exceptions = [ |
| 226 os.path.normcase(os.path.realpath(path)) |
| 227 for path in exceptions |
| 228 ] |
| 229 AbstractSandbox.__init__(self) |
| 230 |
| 231 def _violation(self, operation, *args, **kw): |
| 232 raise SandboxViolation(operation, args, kw) |
| 233 |
| 234 if _file: |
| 235 def _file(self, path, mode='r', *args, **kw): |
| 236 if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): |
| 237 self._violation("file", path, mode, *args, **kw) |
| 238 return _file(path,mode,*args,**kw) |
| 239 |
| 240 def _open(self, path, mode='r', *args, **kw): |
| 241 if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): |
| 242 self._violation("open", path, mode, *args, **kw) |
| 243 return _open(path,mode,*args,**kw) |
| 244 |
| 245 def tmpnam(self): |
| 246 self._violation("tmpnam") |
| 247 |
| 248 def _ok(self, path): |
| 249 active = self._active |
| 250 try: |
| 251 self._active = False |
| 252 realpath = os.path.normcase(os.path.realpath(path)) |
| 253 return ( |
| 254 self._exempted(realpath) |
| 255 or realpath == self._sandbox |
| 256 or realpath.startswith(self._prefix) |
| 257 ) |
| 258 finally: |
| 259 self._active = active |
| 260 |
| 261 def _exempted(self, filepath): |
| 262 start_matches = ( |
| 263 filepath.startswith(exception) |
| 264 for exception in self._exceptions |
| 265 ) |
| 266 pattern_matches = ( |
| 267 re.match(pattern, filepath) |
| 268 for pattern in self._exception_patterns |
| 269 ) |
| 270 candidates = itertools.chain(start_matches, pattern_matches) |
| 271 return any(candidates) |
| 272 |
| 273 def _remap_input(self, operation, path, *args, **kw): |
| 274 """Called for path inputs""" |
| 275 if operation in self.write_ops and not self._ok(path): |
| 276 self._violation(operation, os.path.realpath(path), *args, **kw) |
| 277 return path |
| 278 |
| 279 def _remap_pair(self, operation, src, dst, *args, **kw): |
| 280 """Called for path pairs like rename, link, and symlink operations""" |
| 281 if not self._ok(src) or not self._ok(dst): |
| 282 self._violation(operation, src, dst, *args, **kw) |
| 283 return (src,dst) |
| 284 |
| 285 def open(self, file, flags, mode=0o777, *args, **kw): |
| 286 """Called for low-level os.open()""" |
| 287 if flags & WRITE_FLAGS and not self._ok(file): |
| 288 self._violation("os.open", file, flags, mode, *args, **kw) |
| 289 return _os.open(file,flags,mode, *args, **kw) |
| 290 |
| 291 WRITE_FLAGS = functools.reduce( |
| 292 operator.or_, [getattr(_os, a, 0) for a in |
| 293 "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()] |
| 294 ) |
| 295 |
| 296 class SandboxViolation(DistutilsError): |
| 297 """A setup script attempted to modify the filesystem outside the sandbox""" |
| 298 |
| 299 def __str__(self): |
| 300 return """SandboxViolation: %s%r %s |
| 301 |
| 302 The package setup script has attempted to modify files on your system |
| 303 that are not within the EasyInstall build area, and has been aborted. |
| 304 |
| 305 This package cannot be safely installed by EasyInstall, and may not |
| 306 support alternate installation locations even if you run its setup |
| 307 script by hand. Please inform the package's author and the EasyInstall |
| 308 maintainers to find out if a fix or workaround is available.""" % self.args |
| 309 |
| 310 |
| 311 |
| 312 |
| 313 |
| 314 |
| 315 |
| 316 |
| 317 |
| 318 |
| 319 |
| 320 |
| 321 |
| 322 |
| 323 |
| 324 |
| 325 |
| 326 |
| 327 |
| 328 |
| 329 |
| 330 |
| 331 |
| 332 |
| 333 |
| 334 |
| 335 |
| 336 # |
OLD | NEW |