paths.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. # encoding: utf-8
  2. """Path utility functions."""
  3. # Copyright (c) Jupyter Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. # Derived from IPython.utils.path, which is
  6. # Copyright (c) IPython Development Team.
  7. # Distributed under the terms of the Modified BSD License.
  8. import os
  9. import sys
  10. import stat
  11. import errno
  12. import tempfile
  13. import warnings
  14. from ipython_genutils import py3compat
  15. from contextlib import contextmanager
  16. from distutils.util import strtobool
  17. from ipython_genutils import py3compat
  18. pjoin = os.path.join
  19. # UF_HIDDEN is a stat flag not defined in the stat module.
  20. # It is used by BSD to indicate hidden files.
  21. UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
  22. def get_home_dir():
  23. """Get the real path of the home directory"""
  24. homedir = os.path.expanduser('~')
  25. # Next line will make things work even when /home/ is a symlink to
  26. # /usr/home as it is on FreeBSD, for example
  27. homedir = os.path.realpath(homedir)
  28. homedir = py3compat.str_to_unicode(homedir, encoding=sys.getfilesystemencoding())
  29. return homedir
  30. _dtemps = {}
  31. def _mkdtemp_once(name):
  32. """Make or reuse a temporary directory.
  33. If this is called with the same name in the same process, it will return
  34. the same directory.
  35. """
  36. try:
  37. return _dtemps[name]
  38. except KeyError:
  39. d = _dtemps[name] = tempfile.mkdtemp(prefix=name + '-')
  40. return d
  41. def jupyter_config_dir():
  42. """Get the Jupyter config directory for this platform and user.
  43. Returns JUPYTER_CONFIG_DIR if defined, else ~/.jupyter
  44. """
  45. env = os.environ
  46. home_dir = get_home_dir()
  47. if env.get('JUPYTER_NO_CONFIG'):
  48. return _mkdtemp_once('jupyter-clean-cfg')
  49. if env.get('JUPYTER_CONFIG_DIR'):
  50. return env['JUPYTER_CONFIG_DIR']
  51. return pjoin(home_dir, '.jupyter')
  52. def jupyter_data_dir():
  53. """Get the config directory for Jupyter data files.
  54. These are non-transient, non-configuration files.
  55. Returns JUPYTER_DATA_DIR if defined, else a platform-appropriate path.
  56. """
  57. env = os.environ
  58. if env.get('JUPYTER_DATA_DIR'):
  59. return env['JUPYTER_DATA_DIR']
  60. home = get_home_dir()
  61. if sys.platform == 'darwin':
  62. return os.path.join(home, 'Library', 'Jupyter')
  63. elif os.name == 'nt':
  64. appdata = os.environ.get('APPDATA', None)
  65. if appdata:
  66. return pjoin(appdata, 'jupyter')
  67. else:
  68. return pjoin(jupyter_config_dir(), 'data')
  69. else:
  70. # Linux, non-OS X Unix, AIX, etc.
  71. xdg = env.get("XDG_DATA_HOME", None)
  72. if not xdg:
  73. xdg = pjoin(home, '.local', 'share')
  74. return pjoin(xdg, 'jupyter')
  75. def jupyter_runtime_dir():
  76. """Return the runtime dir for transient jupyter files.
  77. Returns JUPYTER_RUNTIME_DIR if defined.
  78. The default is now (data_dir)/runtime on all platforms;
  79. we no longer use XDG_RUNTIME_DIR after various problems.
  80. """
  81. env = os.environ
  82. if env.get('JUPYTER_RUNTIME_DIR'):
  83. return env['JUPYTER_RUNTIME_DIR']
  84. return pjoin(jupyter_data_dir(), 'runtime')
  85. if os.name == 'nt':
  86. programdata = os.environ.get('PROGRAMDATA', None)
  87. if programdata:
  88. SYSTEM_JUPYTER_PATH = [pjoin(programdata, 'jupyter')]
  89. else: # PROGRAMDATA is not defined by default on XP.
  90. SYSTEM_JUPYTER_PATH = [os.path.join(sys.prefix, 'share', 'jupyter')]
  91. else:
  92. SYSTEM_JUPYTER_PATH = [
  93. "/usr/local/share/jupyter",
  94. "/usr/share/jupyter",
  95. ]
  96. ENV_JUPYTER_PATH = [os.path.join(sys.prefix, 'share', 'jupyter')]
  97. def jupyter_path(*subdirs):
  98. """Return a list of directories to search for data files
  99. JUPYTER_PATH environment variable has highest priority.
  100. If ``*subdirs`` are given, that subdirectory will be added to each element.
  101. Examples:
  102. >>> jupyter_path()
  103. ['~/.local/jupyter', '/usr/local/share/jupyter']
  104. >>> jupyter_path('kernels')
  105. ['~/.local/jupyter/kernels', '/usr/local/share/jupyter/kernels']
  106. """
  107. paths = []
  108. # highest priority is env
  109. if os.environ.get('JUPYTER_PATH'):
  110. paths.extend(
  111. p.rstrip(os.sep)
  112. for p in os.environ['JUPYTER_PATH'].split(os.pathsep)
  113. )
  114. # then user dir
  115. paths.append(jupyter_data_dir())
  116. # then sys.prefix
  117. for p in ENV_JUPYTER_PATH:
  118. if p not in SYSTEM_JUPYTER_PATH:
  119. paths.append(p)
  120. # finally, system
  121. paths.extend(SYSTEM_JUPYTER_PATH)
  122. # add subdir, if requested
  123. if subdirs:
  124. paths = [ pjoin(p, *subdirs) for p in paths ]
  125. return paths
  126. if os.name == 'nt':
  127. programdata = os.environ.get('PROGRAMDATA', None)
  128. if programdata:
  129. SYSTEM_CONFIG_PATH = [os.path.join(programdata, 'jupyter')]
  130. else: # PROGRAMDATA is not defined by default on XP.
  131. SYSTEM_CONFIG_PATH = []
  132. else:
  133. SYSTEM_CONFIG_PATH = [
  134. "/usr/local/etc/jupyter",
  135. "/etc/jupyter",
  136. ]
  137. ENV_CONFIG_PATH = [os.path.join(sys.prefix, 'etc', 'jupyter')]
  138. def jupyter_config_path():
  139. """Return the search path for Jupyter config files as a list."""
  140. paths = [jupyter_config_dir()]
  141. if os.environ.get('JUPYTER_NO_CONFIG'):
  142. return paths
  143. # highest priority is env
  144. if os.environ.get('JUPYTER_CONFIG_PATH'):
  145. paths.extend(
  146. p.rstrip(os.sep)
  147. for p in os.environ['JUPYTER_CONFIG_PATH'].split(os.pathsep)
  148. )
  149. # then sys.prefix
  150. for p in ENV_CONFIG_PATH:
  151. if p not in SYSTEM_CONFIG_PATH:
  152. paths.append(p)
  153. paths.extend(SYSTEM_CONFIG_PATH)
  154. return paths
  155. def exists(path):
  156. """Replacement for `os.path.exists` which works for host mapped volumes
  157. on Windows containers
  158. """
  159. try:
  160. os.lstat(path)
  161. except OSError:
  162. return False
  163. return True
  164. def is_file_hidden_win(abs_path, stat_res=None):
  165. """Is a file hidden?
  166. This only checks the file itself; it should be called in combination with
  167. checking the directory containing the file.
  168. Use is_hidden() instead to check the file and its parent directories.
  169. Parameters
  170. ----------
  171. abs_path : unicode
  172. The absolute path to check.
  173. stat_res : os.stat_result, optional
  174. Ignored on Windows, exists for compatibility with POSIX version of the
  175. function.
  176. """
  177. if os.path.basename(abs_path).startswith('.'):
  178. return True
  179. win32_FILE_ATTRIBUTE_HIDDEN = 0x02
  180. import ctypes
  181. try:
  182. attrs = ctypes.windll.kernel32.GetFileAttributesW(
  183. py3compat.cast_unicode(abs_path)
  184. )
  185. except AttributeError:
  186. pass
  187. else:
  188. if attrs > 0 and attrs & win32_FILE_ATTRIBUTE_HIDDEN:
  189. return True
  190. return False
  191. def is_file_hidden_posix(abs_path, stat_res=None):
  192. """Is a file hidden?
  193. This only checks the file itself; it should be called in combination with
  194. checking the directory containing the file.
  195. Use is_hidden() instead to check the file and its parent directories.
  196. Parameters
  197. ----------
  198. abs_path : unicode
  199. The absolute path to check.
  200. stat_res : os.stat_result, optional
  201. The result of calling stat() on abs_path. If not passed, this function
  202. will call stat() internally.
  203. """
  204. if os.path.basename(abs_path).startswith('.'):
  205. return True
  206. if stat_res is None or stat.S_ISLNK(stat_res.st_mode):
  207. try:
  208. stat_res = os.stat(abs_path)
  209. except OSError as e:
  210. if e.errno == errno.ENOENT:
  211. return False
  212. raise
  213. # check that dirs can be listed
  214. if stat.S_ISDIR(stat_res.st_mode):
  215. # use x-access, not actual listing, in case of slow/large listings
  216. if not os.access(abs_path, os.X_OK | os.R_OK):
  217. return True
  218. # check UF_HIDDEN
  219. if getattr(stat_res, 'st_flags', 0) & UF_HIDDEN:
  220. return True
  221. return False
  222. if sys.platform == 'win32':
  223. is_file_hidden = is_file_hidden_win
  224. else:
  225. is_file_hidden = is_file_hidden_posix
  226. def is_hidden(abs_path, abs_root=''):
  227. """Is a file hidden or contained in a hidden directory?
  228. This will start with the rightmost path element and work backwards to the
  229. given root to see if a path is hidden or in a hidden directory. Hidden is
  230. determined by either name starting with '.' or the UF_HIDDEN flag as
  231. reported by stat.
  232. If abs_path is the same directory as abs_root, it will be visible even if
  233. that is a hidden folder. This only checks the visibility of files
  234. and directories *within* abs_root.
  235. Parameters
  236. ----------
  237. abs_path : unicode
  238. The absolute path to check for hidden directories.
  239. abs_root : unicode
  240. The absolute path of the root directory in which hidden directories
  241. should be checked for.
  242. """
  243. if os.path.normpath(abs_path) == os.path.normpath(abs_root):
  244. return False
  245. if is_file_hidden(abs_path):
  246. return True
  247. if not abs_root:
  248. abs_root = abs_path.split(os.sep, 1)[0] + os.sep
  249. inside_root = abs_path[len(abs_root):]
  250. if any(part.startswith('.') for part in inside_root.split(os.sep)):
  251. return True
  252. # check UF_HIDDEN on any location up to root.
  253. # is_file_hidden() already checked the file, so start from its parent dir
  254. path = os.path.dirname(abs_path)
  255. while path and path.startswith(abs_root) and path != abs_root:
  256. if not exists(path):
  257. path = os.path.dirname(path)
  258. continue
  259. try:
  260. # may fail on Windows junctions
  261. st = os.lstat(path)
  262. except OSError:
  263. return True
  264. if getattr(st, 'st_flags', 0) & UF_HIDDEN:
  265. return True
  266. path = os.path.dirname(path)
  267. return False
  268. def win32_restrict_file_to_user(fname):
  269. """Secure a windows file to read-only access for the user.
  270. Follows guidance from win32 library creator:
  271. http://timgolden.me.uk/python/win32_how_do_i/add-security-to-a-file.html
  272. This method should be executed against an already generated file which
  273. has no secrets written to it yet.
  274. Parameters
  275. ----------
  276. fname : unicode
  277. The path to the file to secure
  278. """
  279. import win32api
  280. import win32security
  281. import ntsecuritycon as con
  282. # everyone, _domain, _type = win32security.LookupAccountName("", "Everyone")
  283. admins = win32security.CreateWellKnownSid(win32security.WinBuiltinAdministratorsSid)
  284. user, _domain, _type = win32security.LookupAccountName("", win32api.GetUserNameEx(win32api.NameSamCompatible))
  285. sd = win32security.GetFileSecurity(fname, win32security.DACL_SECURITY_INFORMATION)
  286. dacl = win32security.ACL()
  287. # dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, everyone)
  288. dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_GENERIC_READ | con.FILE_GENERIC_WRITE, user)
  289. dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, admins)
  290. sd.SetSecurityDescriptorDacl(1, dacl, 0)
  291. win32security.SetFileSecurity(fname, win32security.DACL_SECURITY_INFORMATION, sd)
  292. def get_file_mode(fname):
  293. """Retrieves the file mode corresponding to fname in a filesystem-tolerant manner.
  294. Parameters
  295. ----------
  296. fname : unicode
  297. The path to the file to get mode from
  298. """
  299. # Some filesystems (e.g., CIFS) auto-enable the execute bit on files. As a result, we
  300. # should tolerate the execute bit on the file's owner when validating permissions - thus
  301. # the missing least significant bit on the third octal digit. In addition, we also tolerate
  302. # the sticky bit being set, so the lsb from the fourth octal digit is also removed.
  303. return stat.S_IMODE(os.stat(fname).st_mode) & 0o6677 # Use 4 octal digits since S_IMODE does the same
  304. allow_insecure_writes = strtobool(os.getenv('JUPYTER_ALLOW_INSECURE_WRITES', 'false'))
  305. @contextmanager
  306. def secure_write(fname, binary=False):
  307. """Opens a file in the most restricted pattern available for
  308. writing content. This limits the file mode to `0o0600` and yields
  309. the resulting opened filed handle.
  310. Parameters
  311. ----------
  312. fname : unicode
  313. The path to the file to write
  314. binary: boolean
  315. Indicates that the file is binary
  316. """
  317. mode = 'wb' if binary else 'w'
  318. open_flag = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
  319. try:
  320. os.remove(fname)
  321. except (IOError, OSError):
  322. # Skip any issues with the file not existing
  323. pass
  324. if os.name == 'nt':
  325. if allow_insecure_writes:
  326. # Mounted file systems can have a number of failure modes inside this block.
  327. # For windows machines in insecure mode we simply skip this to avoid failures :/
  328. issue_insecure_write_warning()
  329. else:
  330. # Python on windows does not respect the group and public bits for chmod, so we need
  331. # to take additional steps to secure the contents.
  332. # Touch file pre-emptively to avoid editing permissions in open files in Windows
  333. fd = os.open(fname, open_flag, 0o0600)
  334. os.close(fd)
  335. open_flag = os.O_WRONLY | os.O_TRUNC
  336. win32_restrict_file_to_user(fname)
  337. with os.fdopen(os.open(fname, open_flag, 0o0600), mode) as f:
  338. if os.name != 'nt':
  339. # Enforce that the file got the requested permissions before writing
  340. file_mode = get_file_mode(fname)
  341. if 0o0600 != file_mode:
  342. if allow_insecure_writes:
  343. issue_insecure_write_warning()
  344. else:
  345. raise RuntimeError("Permissions assignment failed for secure file: '{file}'."
  346. " Got '{permissions}' instead of '0o0600'."
  347. .format(file=fname, permissions=oct(file_mode)))
  348. yield f
  349. def issue_insecure_write_warning():
  350. def format_warning(msg, *args, **kwargs):
  351. return str(msg) + '\n'
  352. warnings.formatwarning = format_warning
  353. warnings.warn("WARNING: Insecure writes have been enabled via environment variable "
  354. "'JUPYTER_ALLOW_INSECURE_WRITES'! If this is not intended, remove the "
  355. "variable or set its value to 'False'.")