PageRenderTime 67ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/gateone/core/utils.py

http://github.com/liftoff/GateOne
Python | 1681 lines | 1627 code | 9 blank | 45 comment | 45 complexity | 192415709fd1f44bbfe15a105784a7ff MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright 2013 Liftoff Software Corporation
  4. #
  5. # For license information see LICENSE.txt
  6. __doc__ = """\
  7. .. _utils.py:
  8. Gate One Utility Functions and Classes
  9. ======================================
  10. """
  11. # Meta
  12. __license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
  13. __author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
  14. # Import stdlib stuff
  15. import os
  16. import signal
  17. import sys
  18. import random
  19. import re
  20. import io
  21. import errno
  22. import logging
  23. import mimetypes
  24. import fcntl
  25. import hmac, hashlib
  26. from datetime import datetime, timedelta
  27. from functools import partial
  28. try:
  29. import cPickle as pickle
  30. except ImportError:
  31. import pickle # Python 3
  32. # Import 3rd party stuff
  33. from tornado import locale
  34. from tornado.escape import json_encode as _json_encode
  35. from tornado.escape import to_unicode
  36. from tornado.ioloop import IOLoop, PeriodicCallback
  37. # Globals
  38. MACOS = os.uname()[0] == 'Darwin'
  39. OPENBSD = os.uname()[0] == 'OpenBSD'
  40. CSS_END = re.compile('\.css.*?$')
  41. JS_END = re.compile('\.js.*?$')
  42. # This is used by the raw() function to show control characters
  43. REPLACEMENT_DICT = {
  44. 0: u'^@',
  45. 1: u'^A',
  46. 2: u'^B',
  47. 3: u'^C',
  48. 4: u'^D',
  49. 5: u'^E',
  50. 6: u'^F',
  51. 7: u'^G',
  52. 8: u'^H',
  53. 9: u'^I',
  54. #10: u'^J', # Newline (\n)
  55. 11: u'^K',
  56. 12: u'^L',
  57. #13: u'^M', # Carriage return (\r)
  58. 14: u'^N',
  59. 15: u'^O',
  60. 16: u'^P',
  61. 17: u'^Q',
  62. 18: u'^R',
  63. 19: u'^S',
  64. 20: u'^T',
  65. 21: u'^U',
  66. 22: u'^V',
  67. 23: u'^W',
  68. 24: u'^X',
  69. 25: u'^Y',
  70. 26: u'^Z',
  71. 27: u'^[',
  72. 28: u'^\\',
  73. 29: u'^]',
  74. 30: u'^^',
  75. 31: u'^_',
  76. 127: u'^?',
  77. }
  78. SEPARATOR = u"\U000f0f0f" # The character used to separate frames in the log
  79. # Default to using the environment's locale with en_US fallback
  80. temp_locale = locale.get(os.environ.get('LANG', 'en_US').split('.')[0])
  81. _ = temp_locale.translate
  82. del temp_locale
  83. # The above is necessary because gateone.py won't have read in its settings
  84. # until after this file has loaded. So get_settings() won't work properly
  85. # until later in the module loading process. This lets us display translated
  86. # error messages in the event that Gate One never completed loading.
  87. # Exceptions
  88. class MimeTypeFail(Exception):
  89. """
  90. Raised by `create_data_uri` if the mimetype of a file could not be guessed.
  91. """
  92. pass
  93. class SSLGenerationError(Exception):
  94. """
  95. Raised by `gen_self_signed_ssl` if an error is encountered generating a
  96. self-signed SSL certificate.
  97. """
  98. pass
  99. class ChownError(Exception):
  100. """
  101. Raised by `recursive_chown` if an OSError is encountered while trying to
  102. recursively chown a directory.
  103. """
  104. pass
  105. class AutoExpireDict(dict):
  106. """
  107. An override of Python's `dict` that expires keys after a given
  108. *_expire_timeout* timeout (`datetime.timedelta`). The default expiration
  109. is one hour. It is used like so::
  110. >>> expiring_dict = AutoExpireDict(timeout=timedelta(minutes=10))
  111. >>> expiring_dict['somekey'] = 'some value'
  112. >>> # You can see when this key was created:
  113. >>> print(expiring_dict.creation_times['somekey'])
  114. 2013-04-15 18:44:18.224072
  115. 10 minutes later your key will be gone::
  116. >>> 'somekey' in expiring_dict
  117. False
  118. The 'timeout' may be be given as a `datetime.timedelta` object or a string
  119. like, "1d", "30s" (will be passed through the `convert_to_timedelta`
  120. function).
  121. By default `AutoExpireDict` will check for expired keys every 30 seconds but
  122. this can be changed by setting the 'interval'::
  123. >>> expiring_dict = AutoExpireDict(interval=5000) # 5 secs
  124. >>> # Or to change it after you've created one:
  125. >>> expiring_dict.interval = "10s"
  126. The 'interval' may be an integer, a `datetime.timedelta` object, or a string
  127. such as '10s' or '5m' (will be passed through the `convert_to_timedelta`
  128. function).
  129. If there are no keys remaining the `tornado.ioloop.PeriodicCallback` (
  130. ``self._key_watcher``) that checks expiration will be automatically stopped.
  131. As soon as a new key is added it will be started back up again.
  132. .. note::
  133. Only works if there's a running instances of `tornado.ioloop.IOLoop`.
  134. """
  135. def __init__(self, *args, **kwargs):
  136. self.io_loop = IOLoop.current()
  137. self.creation_times = {}
  138. if 'timeout' in kwargs:
  139. self.timeout = kwargs.pop('timeout')
  140. if 'interval' in kwargs:
  141. self.interval = kwargs.pop('interval')
  142. super(AutoExpireDict, self).__init__(*args, **kwargs)
  143. # Set the start time on every key
  144. for k in self.keys():
  145. self.creation_times[k] = datetime.now()
  146. self._key_watcher = PeriodicCallback(
  147. self._timeout_checker, self.interval, io_loop=self.io_loop)
  148. self._key_watcher.start() # Will shut down at the next interval if empty
  149. @property
  150. def timeout(self):
  151. """
  152. A `property` that controls how long a key will last before being
  153. automatically removed. May be be given as a `datetime.timedelta`
  154. object or a string like, "1d", "30s" (will be passed through the
  155. `convert_to_timedelta` function).
  156. """
  157. if not hasattr(self, "_timeout"):
  158. self._timeout = timedelta(hours=1) # Default is 1-hour timeout
  159. return self._timeout
  160. @timeout.setter
  161. def timeout(self, value):
  162. if isinstance(value, basestring):
  163. value = convert_to_timedelta(value)
  164. self._timeout = value
  165. @property
  166. def interval(self):
  167. """
  168. A `property` that controls how often we check for expired keys. May be
  169. given as milliseconds (integer), a `datetime.timedelta` object, or a
  170. string like, "1d", "30s" (will be passed through the
  171. `convert_to_timedelta` function).
  172. """
  173. if not hasattr(self, "_interval"):
  174. self._interval = 10000 # Default is every 10 seconds
  175. return self._interval
  176. @interval.setter
  177. def interval(self, value):
  178. if isinstance(value, basestring):
  179. value = convert_to_timedelta(value)
  180. if isinstance(value, timedelta):
  181. value = total_seconds(value) * 1000 # PeriodicCallback uses ms
  182. self._interval = value
  183. # Restart the PeriodicCallback
  184. if hasattr(self, '_key_watcher'):
  185. self._key_watcher.stop()
  186. self._key_watcher = PeriodicCallback(
  187. self._timeout_checker, value, io_loop=self.io_loop)
  188. def renew(self, key):
  189. """
  190. Resets the timeout on the given *key*; like it was just created.
  191. """
  192. self.creation_times[key] = datetime.now() # Set/renew the start time
  193. # Start up the key watcher if it isn't already running
  194. if not self._key_watcher._running:
  195. self._key_watcher.start()
  196. def __setitem__(self, key, value):
  197. """
  198. An override that tracks when keys are updated.
  199. """
  200. super(AutoExpireDict, self).__setitem__(key, value) # Set normally
  201. self.renew(key) # Set/renew the start time
  202. def __delitem__(self, key):
  203. """
  204. An override that makes sure *key* gets removed from
  205. ``self.creation_times`` dict.
  206. """
  207. del self.creation_times[key]
  208. super(AutoExpireDict, self).__delitem__(key)
  209. def __del__(self):
  210. """
  211. Ensures that our `tornado.ioloop.PeriodicCallback`
  212. (``self._key_watcher``) gets stopped.
  213. """
  214. self._key_watcher.stop()
  215. def update(self, *args, **kwargs):
  216. """
  217. An override that calls ``self.renew()`` for every key that gets updated.
  218. """
  219. super(AutoExpireDict, self).update(*args, **kwargs)
  220. for key, value in kwargs.items():
  221. self.renew(key)
  222. def clear(self):
  223. """
  224. An override that empties ``self.creation_times`` and calls
  225. ``self._key_watcher.stop()``.
  226. """
  227. super(AutoExpireDict, self).clear()
  228. self.creation_times.clear()
  229. # Shut down the key watcher right away
  230. self._key_watcher.stop()
  231. def _timeout_checker(self):
  232. """
  233. Walks ``self`` and removes keys that have passed the expiration point.
  234. """
  235. if not self.creation_times:
  236. self._key_watcher.stop() # Nothing left to watch
  237. for key, starttime in list(self.creation_times.items()):
  238. if datetime.now() - starttime > self.timeout:
  239. del self[key]
  240. MEMO = {}
  241. class memoize(object):
  242. """
  243. A memoization decorator that works with multiple arguments as well as
  244. unhashable arguments (e.g. dicts). It also self-expires any memoized
  245. calls after the timedelta specified via *timeout*.
  246. If a *timeout* is not given memoized information will be discared after five
  247. minutes.
  248. .. note:: Expiration checks will be performed every 30 seconds.
  249. """
  250. def __init__(self, fn, timeout=None):
  251. self.fn = fn
  252. if not timeout:
  253. timeout = timedelta(minutes=5)
  254. global MEMO # Use a global so that instances can share the cache
  255. if not MEMO:
  256. MEMO = AutoExpireDict(timeout=timeout, interval="30s")
  257. def __call__(self, *args, **kwargs):
  258. string = pickle.dumps(args, 0) + pickle.dumps(kwargs, 0)
  259. if string not in MEMO:
  260. # Commented out because it is REALLY noisy. Uncomment to debug
  261. #logging.debug("memoize cache miss (%s)" % self.fn.__name__)
  262. MEMO[string] = self.fn(*args, **kwargs)
  263. #else:
  264. #logging.debug("memoize cache hit (%s)" % self.fn.__name__)
  265. return MEMO[string]
  266. # Functions
  267. def noop(*args, **kwargs):
  268. """Do nothing (i.e. "No Operation")"""
  269. pass
  270. def debug_info(name, *args, **kwargs):
  271. """
  272. This function returns a string like this::
  273. >>> debug_info('some_function', 5, 10, foo="bar")
  274. 'some_function(5, 10, foo="bar")'
  275. Primarily aimed at debugging.
  276. """
  277. out = name + "("
  278. for arg in args:
  279. out += "{0}, ".format(repr(arg))
  280. for k, v in kwargs.items():
  281. out += '{0}={1}, '.format(k, repr(v))
  282. return out.rstrip().rstrip(',') + ")"
  283. def write_pid(path):
  284. """Writes our PID to *path*."""
  285. try:
  286. pid = os.getpid()
  287. with io.open(path, mode='w', encoding='utf-8') as pidfile:
  288. # Get a non-blocking exclusive lock
  289. fcntl.flock(pidfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
  290. pidfile.seek(0)
  291. pidfile.truncate(0)
  292. pidfile.write(unicode(pid))
  293. except:
  294. logging.error(_("Could not write PID file: %s") % path)
  295. raise # This raises the original exception
  296. finally:
  297. try:
  298. pidfile.close()
  299. except:
  300. pass
  301. def read_pid(path):
  302. """Reads our current PID from *path*."""
  303. return str(io.open(path, mode='r', encoding='utf-8').read())
  304. def remove_pid(path):
  305. """Removes the PID file at *path*."""
  306. try:
  307. os.remove(path)
  308. except:
  309. pass
  310. def shell_command(cmd, timeout_duration=5):
  311. """
  312. Resets the SIGCHLD signal handler (if necessary), executes *cmd* via
  313. :func:`~commands.getstatusoutput`, then re-enables the SIGCHLD handler (if
  314. it was set to something other than SIG_DFL). Returns the result of
  315. :func:`~commands.getstatusoutput` which is a tuple in the form of::
  316. (exitstatus, output)
  317. If the command takes longer than *timeout_duration* seconds, it will be
  318. auto-killed and the following will be returned::
  319. (255, _("ERROR: Timeout running shell command"))
  320. """
  321. from commands import getstatusoutput
  322. existing_handler = signal.getsignal(signal.SIGCHLD)
  323. default = (255, _("ERROR: Timeout running shell command"))
  324. if existing_handler != 0: # Something other than default
  325. # Reset it to default so getstatusoutput will work properly
  326. try:
  327. signal.signal(signal.SIGCHLD, signal.SIG_DFL)
  328. except ValueError:
  329. # "Signal only works in the main thread" - no big deal. This just
  330. # means we never needed to call signal in the first place.
  331. pass
  332. result = timeout_func(
  333. getstatusoutput,
  334. args=(cmd,),
  335. default=default,
  336. timeout_duration=timeout_duration
  337. )
  338. try:
  339. signal.signal(signal.SIGCHLD, existing_handler)
  340. except ValueError:
  341. # Like above, signal only works from within the main thread but our use
  342. # of it here would only matter if we were in the main thread.
  343. pass
  344. return result
  345. def json_encode(obj):
  346. """
  347. On some platforms (CentOS 6.2, specifically) `tornado.escape.json_decode`
  348. doesn't seem to work just right when it comes to returning unicode strings.
  349. This is just a wrapper that ensures that the returned string is unicode.
  350. """
  351. return to_unicode(_json_encode(obj))
  352. def gen_self_signed_ssl(path=None):
  353. """
  354. Generates a self-signed SSL certificate using `pyOpenSSL` or the
  355. `openssl <http://www.openssl.org/docs/apps/openssl.html>`_ command
  356. depending on what's available, The resulting key/certificate will use the
  357. RSA algorithm at 4096 bits.
  358. """
  359. try:
  360. import OpenSSL
  361. # Direct OpenSSL library calls are better than executing commands...
  362. gen_self_signed_func = gen_self_signed_pyopenssl
  363. except ImportError:
  364. gen_self_signed_func = gen_self_signed_openssl
  365. try:
  366. gen_self_signed_func(path=path)
  367. except SSLGenerationError as e:
  368. logging.error(_(
  369. "Error generating self-signed SSL key/certificate: %s" % e))
  370. def gen_self_signed_openssl(path=None):
  371. """
  372. This method will generate a secure self-signed SSL key/certificate pair
  373. (using the `openssl <http://www.openssl.org/docs/apps/openssl.html>`_
  374. command) saving the result as 'certificate.pem' and 'keyfile.pem' to *path*.
  375. If *path* is not given the result will be saved in the current working
  376. directory. The certificate will be valid for 10 years.
  377. .. note:: The self-signed certificate will utilize 4096-bit RSA encryption.
  378. """
  379. if not path:
  380. path = os.path.abspath(os.curdir)
  381. keyfile_path = os.path.join(path, "keyfile.pem")
  382. certfile_path = os.path.join(path, "certificate.pem")
  383. subject = (
  384. '-subj "/OU=%s (Self-Signed)/CN=Gate One/O=Liftoff Software"' %
  385. os.uname()[1] # Hostname
  386. )
  387. gen_command = (
  388. "openssl genrsa -aes256 -out %s.tmp -passout pass:password 4096" %
  389. keyfile_path
  390. )
  391. decrypt_key_command = (
  392. "openssl rsa -in {0}.tmp -passin pass:password -out {0}".format(
  393. keyfile_path)
  394. )
  395. csr_command = (
  396. "openssl req -new -key %s -out temp.csr %s" % (
  397. keyfile_path, subject)
  398. )
  399. cert_command = (
  400. "openssl x509 -req " # Create a new x509 certificate
  401. "-days 3650 " # That lasts 10 years
  402. "-in temp.csr " # Using the CSR we just generated
  403. "-signkey %s " # Sign it with keyfile.pem that we just created
  404. "-out %s" # Save it as certificate.pem
  405. )
  406. cert_command = cert_command % (keyfile_path, certfile_path)
  407. logging.debug(_(
  408. "Generating private key with command: %s" % gen_command))
  409. exitstatus, output = shell_command(gen_command, 30)
  410. if exitstatus != 0:
  411. error_msg = _(
  412. "An error occurred trying to create private SSL key:\n%s" % output)
  413. if os.path.exists('%s.tmp' % keyfile_path):
  414. os.remove('%s.tmp' % keyfile_path)
  415. raise SSLGenerationError(error_msg)
  416. logging.debug(_(
  417. "Decrypting private key with command: %s" % decrypt_key_command))
  418. exitstatus, output = shell_command(decrypt_key_command, 30)
  419. if exitstatus != 0:
  420. error_msg = _(
  421. "An error occurred trying to decrypt private SSL key:\n%s" % output)
  422. if os.path.exists('%s.tmp' % keyfile_path):
  423. os.remove('%s.tmp' % keyfile_path)
  424. raise SSLGenerationError(error_msg)
  425. logging.debug(_(
  426. "Creating CSR with command: %s" % csr_command))
  427. exitstatus, output = shell_command(csr_command, 30)
  428. if exitstatus != 0:
  429. error_msg = _(
  430. "An error occurred trying to create CSR:\n%s" % output)
  431. if os.path.exists('%s.tmp' % keyfile_path):
  432. os.remove('%s.tmp' % keyfile_path)
  433. if os.path.exists('temp.csr'):
  434. os.remove('temp.csr')
  435. raise SSLGenerationError(error_msg)
  436. logging.debug(_(
  437. "Generating self-signed certificate with command: %s" % gen_command))
  438. exitstatus, output = shell_command(cert_command, 30)
  439. if exitstatus != 0:
  440. error_msg = _(
  441. "An error occurred trying to create certificate:\n%s" % output)
  442. if os.path.exists('%s.tmp' % keyfile_path):
  443. os.remove('%s.tmp' % keyfile_path)
  444. if os.path.exists('temp.csr'):
  445. os.remove('temp.csr')
  446. if os.path.exists(certfile_path):
  447. os.remove(certfile_path)
  448. raise SSLGenerationError(error_msg)
  449. # Clean up unnecessary leftovers
  450. os.remove('%s.tmp' % keyfile_path)
  451. os.remove('temp.csr')
  452. def gen_self_signed_pyopenssl(notAfter=None, path=None):
  453. """
  454. This method will generate a secure self-signed SSL key/certificate pair
  455. (using `pyOpenSSL`) saving the result as 'certificate.pem' and 'keyfile.pem'
  456. in *path*. If *path* is not given the result will be saved in the current
  457. working directory. By default the certificate will be valid for 10 years
  458. but this can be overridden by passing a valid timestamp via the
  459. *notAfter* argument.
  460. Examples::
  461. >>> gen_self_signed_ssl(60 * 60 * 24 * 365) # 1-year certificate
  462. >>> gen_self_signed_ssl() # 10-year certificate
  463. .. note:: The self-signed certificate will utilize 4096-bit RSA encryption.
  464. """
  465. try:
  466. import OpenSSL
  467. except ImportError:
  468. error_msg = _(
  469. "Error: You do not have pyOpenSSL installed. Please install "
  470. "it (sudo pip install pyopenssl.")
  471. raise SSLGenerationError(error_msg)
  472. if not path:
  473. path = os.path.abspath(os.curdir)
  474. keyfile_path = "%s/keyfile.pem" % path
  475. certfile_path = "%s/certificate.pem" % path
  476. pkey = OpenSSL.crypto.PKey()
  477. pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 4096)
  478. # Save the key as 'keyfile.pem':
  479. with io.open(keyfile_path, mode='wb') as f:
  480. f.write(OpenSSL.crypto.dump_privatekey(
  481. OpenSSL.crypto.FILETYPE_PEM, pkey))
  482. cert = OpenSSL.crypto.X509()
  483. cert.set_serial_number(random.randint(0, sys.maxsize))
  484. cert.gmtime_adj_notBefore(0)
  485. if notAfter:
  486. cert.gmtime_adj_notAfter(notAfter)
  487. else:
  488. cert.gmtime_adj_notAfter(60 * 60 * 24 * 3650)
  489. cert.get_subject().CN = '*'
  490. cert.get_subject().O = 'Gate One Certificate'
  491. cert.get_issuer().CN = 'Untrusted Authority'
  492. cert.get_issuer().O = 'Self-Signed'
  493. cert.set_pubkey(pkey)
  494. cert.sign(pkey, 'sha512')
  495. with io.open(certfile_path, mode='wb') as f:
  496. f.write(OpenSSL.crypto.dump_certificate(
  497. OpenSSL.crypto.FILETYPE_PEM, cert))
  498. def none_fix(val):
  499. """
  500. If *val* is a string that utlimately means 'none', return None. Otherwise
  501. return *val* as-is. Examples::
  502. >>> none_fix('none')
  503. None
  504. >>> none_fix('0')
  505. None
  506. >>> none_fix('whatever')
  507. 'whatever'
  508. """
  509. if isinstance(val, basestring) and val.lower() in ['none', '0', 'no']:
  510. return None
  511. else:
  512. return val
  513. def str2bool(val):
  514. """
  515. Converts strings like, 'false', 'true', '0', and '1' into their boolean
  516. equivalents (in Python). If no logical match is found, return False.
  517. Examples::
  518. >>> str2bool('false')
  519. False
  520. >>> str2bool('1')
  521. True
  522. >>> str2bool('whatever')
  523. False
  524. """
  525. if isinstance(val, basestring) and val.lower() in ['1', 'true', 'yes']:
  526. return True
  527. else:
  528. return False
  529. def generate_session_id():
  530. """
  531. Returns a random, 45-character session ID. Example:
  532. .. code-block:: python
  533. >>> generate_session_id()
  534. "NzY4YzFmNDdhMTM1NDg3Y2FkZmZkMWJmYjYzNjBjM2Y5O"
  535. >>>
  536. """
  537. import base64, uuid
  538. from tornado.escape import utf8
  539. session_id = base64.b64encode(
  540. utf8(uuid.uuid4().hex + uuid.uuid4().hex))[:45]
  541. if bytes != str: # Python 3
  542. return str(session_id, 'UTF-8')
  543. return session_id
  544. def mkdir_p(path):
  545. """
  546. Pythonic version of "mkdir -p". Example equivalents::
  547. >>> mkdir_p('/tmp/test/testing') # Does the same thing as...
  548. >>> from subprocess import call
  549. >>> call('mkdir -p /tmp/test/testing')
  550. .. note:: This doesn't actually call any external commands.
  551. """
  552. try:
  553. os.makedirs(path)
  554. except OSError as exc:
  555. if exc.errno == errno.EEXIST:
  556. pass
  557. else:
  558. logging.error(_("Could not create directory: %s") % path)
  559. raise # The original exception
  560. def cmd_var_swap(cmd, **kwargs):
  561. """
  562. Returns *cmd* with %variable% replaced with the keys/values passed in via
  563. *kwargs*. This function is used by Gate One's Terminal application to
  564. swap the following Gate One variables in defined terminal 'commands':
  565. ============== ==============
  566. %SESSION% *session*
  567. %SESSION_DIR% *session_dir*
  568. %SESSION_HASH% *session_hash*
  569. %USERDIR% *user_dir*
  570. %USER% *user*
  571. %TIME% *time*
  572. ============== ==============
  573. This allows for unique or user-specific values to be swapped into command
  574. line arguments like so::
  575. ssh_connect.py -M -S '%SESSION%/%SESSION%/%r@%L:%p'
  576. Could become::
  577. ssh_connect.py -M -S '/tmp/gateone/NWI0YzYxNzAwMTA3NGYyZmI0OWJmODczYmQyMjQwMDYwM/%r@%L:%p'
  578. Here's an example::
  579. >>> cmd = "echo '%FOO% %BAR%'"
  580. >>> cmd_var_swap(cmd, foo="FOOYEAH,", bar="BAR NONE!")
  581. "echo 'FOOYEAH, BAR NONE!'"
  582. .. note::
  583. The variables passed into this function via *kwargs* are case
  584. insensitive. `cmd_var_swap(cmd, session=var)` would produce the same
  585. output as `cmd_var_swap(cmd, SESSION=var)`.
  586. """
  587. for key, value in kwargs.items():
  588. if isinstance(key, bytes):
  589. key = key.decode('utf-8')
  590. if isinstance(value, bytes):
  591. value = value.decode('utf-8')
  592. key = unicode(key) # Force to string in case of things like integers
  593. value = unicode(value)
  594. cmd = cmd.replace(u'%{key}%'.format(key=key.upper()), value)
  595. return cmd
  596. def short_hash(to_shorten):
  597. """
  598. Converts *to_shorten* into a really short hash depenendent on the length of
  599. *to_shorten*. The result will be safe for use as a file name.
  600. .. note::
  601. Collisions are possible but *highly* unlikely because of how this method
  602. is typically used.
  603. """
  604. import base64
  605. hashed = hashlib.sha1(to_shorten.encode('utf-8'))
  606. # Take the first eight characters to create a shortened version.
  607. hashed = base64.urlsafe_b64encode(hashed.digest())[:8].decode('utf-8')
  608. if hashed.startswith('-'):
  609. hashed = hashed.replace('-', 'A', 1)
  610. return hashed
  611. def random_words(n=1):
  612. """
  613. Returns *n* random English words (as a tuple of strings) from the
  614. `english_wordlist.txt` file (bundled with Gate One).
  615. .. note:: In Python 2 the words will be Unicode strings.
  616. """
  617. from pkg_resources import resource_string
  618. words = resource_string(
  619. 'gateone', 'static/english_wordlist.txt').split(b'\n')
  620. out_words = []
  621. for i in range(n):
  622. word = words[random.randint(0, len(words))].lower()
  623. out_words.append(word.decode('utf-8'))
  624. return tuple(out_words)
  625. def get_process_tree(parent_pid):
  626. """
  627. Returns a list of child pids that were spawned from *parent_pid*.
  628. .. note:: Will include parent_pid in the output list.
  629. """
  630. parent_pid = str(parent_pid) # Has to be a string
  631. ps = which('ps')
  632. retcode, output = shell_command('%s -ef' % ps)
  633. out = [parent_pid]
  634. pidmap = []
  635. # Construct the pidmap:
  636. for line in output.splitlines():
  637. split_line = line.split()
  638. pid = split_line[1]
  639. ppid = split_line[2]
  640. pidmap.append((pid, ppid))
  641. def walk_pids(pidmap, checkpid):
  642. """
  643. Recursively walks the given *pidmap* and updates the *out* variable with
  644. the child pids of *checkpid*.
  645. """
  646. for pid, ppid in pidmap:
  647. if ppid == checkpid:
  648. out.append(pid)
  649. walk_pids(pidmap, pid)
  650. walk_pids(pidmap, parent_pid)
  651. return out
  652. def kill_dtached_proc(session, location, term):
  653. """
  654. Kills the dtach processes associated with the given *session* that matches
  655. the given *location* and *term*. All the dtach'd sub-processes will be
  656. killed as well.
  657. """
  658. logging.debug('kill_dtached_proc(%s, %s, %s)' % (session, location, term))
  659. dtach_socket_name = 'dtach_{location}_{term}'.format(
  660. location=location, term=term)
  661. to_kill = []
  662. for f in os.listdir('/proc'):
  663. pid_dir = os.path.join('/proc', f)
  664. if os.path.isdir(pid_dir):
  665. try:
  666. pid = int(f)
  667. except ValueError:
  668. continue # Not a PID
  669. try:
  670. with open(os.path.join(pid_dir, 'cmdline')) as f:
  671. cmdline = f.read()
  672. if cmdline and session in cmdline:
  673. if dtach_socket_name in cmdline:
  674. to_kill.append(pid)
  675. except Exception:
  676. pass # Already dead, no big deal.
  677. for pid in to_kill:
  678. kill_pids = get_process_tree(pid)
  679. for _pid in kill_pids:
  680. _pid = int(_pid)
  681. try:
  682. os.kill(_pid, signal.SIGTERM)
  683. except OSError:
  684. pass # Process already died. Not a problem.
  685. def kill_dtached_proc_bsd(session, location, term):
  686. """
  687. A BSD-specific implementation of `kill_dtached_proc` since Macs don't have
  688. /proc. Seems simpler than :func:`kill_dtached_proc` but actually having to
  689. call a subprocess is less efficient (due to the sophisticated signal
  690. handling required by :func:`shell_command`).
  691. """
  692. logging.debug('kill_dtached_proc_bsd(%s, %s)' % (session, term))
  693. ps = which('ps')
  694. if MACOS:
  695. psopts = "-ef"
  696. elif OPENBSD:
  697. psopts = "-aux"
  698. cmd = (
  699. "%s %s | "
  700. "grep %s/dtach_%s_%s | " # Match our exact session/location/term combo
  701. "grep -v grep | " # Get rid of grep from the results (if present)
  702. "awk '{print $2}' " % (ps, psopts, session, location, term) # Just PID
  703. )
  704. logging.debug('kill cmd: %s' % cmd)
  705. exitstatus, output = shell_command(cmd)
  706. for line in output.splitlines():
  707. pid_to_kill = line.strip() # Get rid of trailing newline
  708. for pid in get_process_tree(pid_to_kill):
  709. try:
  710. os.kill(int(pid), signal.SIGTERM)
  711. except OSError:
  712. pass # Process already died. Not a problem.
  713. def killall(session_dir, pid_file):
  714. """
  715. Kills all running Gate One terminal processes including any detached dtach
  716. sessions.
  717. :session_dir: The path to Gate One's session directory.
  718. :pid_file: The path to Gate One's PID file
  719. """
  720. if not os.path.exists(session_dir):
  721. logging.info(_("No lieutenant, your processes are already dead."))
  722. return # Nothing to do
  723. logging.info(_("Killing all Gate One processes..."))
  724. sessions = os.listdir(session_dir)
  725. for f in os.listdir('/proc'):
  726. pid_dir = os.path.join('/proc', f)
  727. if os.path.isdir(pid_dir):
  728. try:
  729. pid = int(f)
  730. if pid == os.getpid():
  731. continue # It would be suicide!
  732. except ValueError:
  733. continue # Not a PID
  734. cmdline_path = os.path.join(pid_dir, 'cmdline')
  735. if os.path.exists(cmdline_path):
  736. try:
  737. with io.open(cmdline_path, mode='r', encoding='utf-8') as f:
  738. cmdline = f.read()
  739. except IOError:
  740. # Can happen if a process ended as we were looking at it
  741. continue
  742. for session in sessions:
  743. if session in cmdline:
  744. try:
  745. os.kill(pid, signal.SIGTERM)
  746. except OSError:
  747. pass # PID is already dead--great
  748. try:
  749. go_pid = int(io.open(pid_file, mode='r', encoding='utf-8').read())
  750. except:
  751. logging.warning(_(
  752. "Could not open pid_file (%s). You *may* have to kill gateone.py "
  753. "manually (probably not)." % pid_file))
  754. return
  755. try:
  756. os.kill(go_pid, signal.SIGTERM)
  757. except OSError:
  758. pass # PID is already dead--great
  759. def killall_bsd(session_dir, pid_file=None):
  760. """
  761. A BSD-specific version of `killall` since Macs don't have /proc.
  762. .. note::
  763. *pid_file* is not used by this function. It is simply here to provide
  764. compatibility with `killall`.
  765. """
  766. # TODO: See if there's a better way to keep track of subprocesses so we
  767. # don't have to enumerate the process table at all.
  768. logging.debug('killall_bsd(%s)' % session_dir)
  769. sessions = os.listdir(session_dir)
  770. if MACOS:
  771. psopts = "-ef"
  772. elif OPENBSD:
  773. psopts = "-aux"
  774. for session in sessions:
  775. cmd = (
  776. "ps %s | "
  777. "grep %s | " # Limit to those matching the session
  778. "grep -v grep | " # Get rid of grep from the results (if present)
  779. "awk '{print $2}' | " # Just the PID please
  780. "xargs kill" % (psopts, session) # Kill em'
  781. )
  782. logging.debug('killall cmd: %s' % cmd)
  783. exitstatus, output = shell_command(cmd)
  784. def kill_session_processes(session):
  785. """
  786. Kills all processes that match a given *session* (which is a unique,
  787. 45-character string).
  788. """
  789. psopts = "aux"
  790. if MACOS:
  791. psopts = "-ef"
  792. elif OPENBSD:
  793. psopts = "-aux"
  794. cmd = (
  795. "ps %s | "
  796. "grep %s | " # Limit to those matching the session
  797. "grep -v grep | " # Get rid of grep from the results (if present)
  798. "awk '{print $2}' | " # Just the PID please
  799. "xargs kill" % (psopts, session) # Kill em'
  800. )
  801. logging.debug('kill_session_processes cmd: %s' % cmd)
  802. exitstatus, output = shell_command(cmd)
  803. def entry_point_files(ep_group, enabled=None):
  804. """
  805. Given an entry point group name (*ep_group*), returns a dict of available
  806. Python, JS, and CSS plugins for Gate One::
  807. {
  808. 'css': ['editor/static/codemirror.css'],
  809. 'js': ['editor/static/codemirror.js', 'editor/static/editor.js'],
  810. 'py': [<module 'gateone.plugins.editor' from 'gateone/plugins/editor/__init__.pyc'>]
  811. }
  812. Optionally, the returned dict will include only those modules and files for
  813. plugins in the *enabled* list (if given).
  814. .. note::
  815. Python plugins will be imported automatically as part of the
  816. discovery process.
  817. To do this it uses the `pkg_resources` module from setuptools. For plugins
  818. to be imported correctly they need to register themselves using the given
  819. `entry_point` group (*ep_group*) in their setup.py. Gate One (currently)
  820. uses the following entry point group names:
  821. * go_plugins
  822. * go_applications
  823. * go_applications_plugins
  824. ...but this function can return the JS, CSS, and Python modules for any
  825. entry point that uses the same module_name/static/ layout.
  826. """
  827. import pkg_resources, operator
  828. if not enabled:
  829. enabled = []
  830. ep_dict = {
  831. 'py': {},
  832. 'js': {},
  833. 'css': {}
  834. }
  835. for plugin in pkg_resources.iter_entry_points(group=ep_group):
  836. if enabled and plugin.name not in enabled:
  837. continue # Not enabled, skip it
  838. try:
  839. module = plugin.load()
  840. except ImportError:
  841. logging.warning(
  842. _("Could not import entry point module: {0}").format(
  843. plugin.module_name))
  844. continue
  845. ep_dict['py'][plugin.module_name] = module
  846. static_path = plugin.module_name.replace('.', '/') + '/static/'
  847. try:
  848. pkg_files = pkg_resources.resource_listdir(
  849. plugin.module_name, '/static/')
  850. except OSError:
  851. continue
  852. ep_dict['js'][plugin.module_name] = []
  853. ep_dict['css'][plugin.module_name] = []
  854. for f in pkg_files:
  855. f_path = "/static/%s" % f
  856. if f.endswith('.js'):
  857. ep_dict['js'][plugin.module_name].append(f_path)
  858. elif f.endswith('.css'):
  859. ep_dict['css'][plugin.module_name].append(f_path)
  860. return ep_dict
  861. def load_modules(modules):
  862. """
  863. Given a list of Python *modules*, imports them.
  864. .. note:: Assumes they're all in `sys.path`.
  865. """
  866. logging.debug("load_modules(%s)" % modules)
  867. out_list = []
  868. for module in modules:
  869. imported = __import__(module, None, None, [''])
  870. out_list.append(imported)
  871. return out_list
  872. def merge_handlers(handlers):
  873. """
  874. Takes a list of Tornado *handlers* like this::
  875. [
  876. (r"/", MainHandler),
  877. (r"/ws", TerminalWebSocket),
  878. (r"/auth", AuthHandler),
  879. (r"/style", StyleHandler),
  880. ...
  881. (r"/style", SomePluginHandler),
  882. ]
  883. ...and returns a list with duplicate handlers removed; giving precedence to
  884. handlers with higher indexes. This allows plugins to override Gate One's
  885. default handlers. Given the above, this is what would be returned::
  886. [
  887. (r"/", MainHandler),
  888. (r"/ws", TerminalWebSocket),
  889. (r"/auth", AuthHandler),
  890. ...
  891. (r"/style", SomePluginHandler),
  892. ]
  893. This example would replace the default "/style" handler with
  894. SomePluginHandler; overriding Gate One's default StyleHandler.
  895. """
  896. out_list = []
  897. regexes = []
  898. handlers.reverse()
  899. for handler in handlers:
  900. if handler[0] not in regexes:
  901. regexes.append(handler[0])
  902. out_list.append(handler)
  903. out_list.reverse()
  904. return out_list
  905. # NOTE: This function has been released under the Apache 2.0 license.
  906. # See: http://code.activestate.com/recipes/577894-convert-strings-like-5d-and-60s-to-timedelta-objec/
  907. def convert_to_timedelta(time_val):
  908. """
  909. Given a *time_val* (string) such as '5d', returns a `datetime.timedelta`
  910. object representing the given value (e.g. `timedelta(days=5)`). Accepts the
  911. following '<num><char>' formats:
  912. ========= ============ =========================
  913. Character Meaning Example
  914. ========= ============ =========================
  915. (none) Milliseconds '500' -> 500 Milliseconds
  916. s Seconds '60s' -> 60 Seconds
  917. m Minutes '5m' -> 5 Minutes
  918. h Hours '24h' -> 24 Hours
  919. d Days '7d' -> 7 Days
  920. M Months '2M' -> 2 Months
  921. y Years '10y' -> 10 Years
  922. ========= ============ =========================
  923. Examples::
  924. >>> convert_to_timedelta('7d')
  925. datetime.timedelta(7)
  926. >>> convert_to_timedelta('24h')
  927. datetime.timedelta(1)
  928. >>> convert_to_timedelta('60m')
  929. datetime.timedelta(0, 3600)
  930. >>> convert_to_timedelta('120s')
  931. datetime.timedelta(0, 120)
  932. """
  933. try:
  934. num = int(time_val)
  935. return timedelta(milliseconds=num)
  936. except ValueError:
  937. pass
  938. num = int(time_val[:-1])
  939. if time_val.endswith('s'):
  940. return timedelta(seconds=num)
  941. elif time_val.endswith('m'):
  942. return timedelta(minutes=num)
  943. elif time_val.endswith('h'):
  944. return timedelta(hours=num)
  945. elif time_val.endswith('d'):
  946. return timedelta(days=num)
  947. elif time_val.endswith('M'):
  948. return timedelta(days=(num*30)) # Yeah this is approximate
  949. elif time_val.endswith('y'):
  950. return timedelta(days=(num*365)) # Sorry, no leap year support
  951. def convert_to_bytes(size_val):
  952. """
  953. Given a *size_val* (string) such as '100M', returns an integer representing
  954. an equivalent amount of bytes. Accepts the following '<num><char>' formats:
  955. =========== ========== ==================================
  956. Character Meaning Example
  957. =========== ========== ==================================
  958. B (or none) Bytes '100' or '100b' -> 100
  959. K Kilobytes '1k' -> 1024
  960. M Megabytes '1m' -> 1048576
  961. G Gigabytes '1g' -> 1073741824
  962. T Terabytes '1t' -> 1099511627776
  963. P Petabytes '1p' -> 1125899906842624
  964. E Exabytes '1e' -> 1152921504606846976
  965. Z Zettabytes '1z' -> 1180591620717411303424L
  966. Y Yottabytes '7y' -> 1208925819614629174706176L
  967. =========== ========== ==================================
  968. .. note::
  969. If no character is given the *size_val* will be assumed to be in bytes.
  970. .. tip::
  971. All characters will be converted to upper case before conversion
  972. (case-insensitive).
  973. Examples::
  974. >>> convert_to_bytes('2M')
  975. 2097152
  976. >>> convert_to_bytes('2g')
  977. 2147483648
  978. """
  979. symbols = "BKMGTPEZY"
  980. letter = size_val[-1:].strip().upper()
  981. if letter.isdigit(): # Assume bytes
  982. letter = 'B'
  983. num = size_val
  984. else:
  985. num = size_val[:-1]
  986. assert num.isdigit() and letter in symbols
  987. num = float(num)
  988. prefix = {symbols[0]:1}
  989. for i, size_val in enumerate(symbols[1:]):
  990. prefix[size_val] = 1 << (i+1)*10
  991. return int(num * prefix[letter])
  992. def total_seconds(td):
  993. """
  994. Given a timedelta (*td*) return an integer representing the equivalent of
  995. Python 2.7's :meth:`datetime.timdelta.total_seconds`.
  996. """
  997. return (((
  998. td.microseconds +
  999. (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6))
  1000. def process_opt_esc_sequence(chars):
  1001. """
  1002. Parse the *chars* passed from :class:`terminal.Terminal` by way of the
  1003. special, optional escape sequence handler (e.g. '<plugin>|<text>') into a
  1004. tuple of (<plugin name>, <text>). Here's an example::
  1005. >>> process_opt_esc_sequence('ssh|user@host:22')
  1006. ('ssh', 'user@host:22')
  1007. """
  1008. plugin = None
  1009. text = ""
  1010. try:
  1011. plugin, text = chars.split('|')
  1012. except Exception:
  1013. pass # Something went horribly wrong!
  1014. return (plugin, text)
  1015. def raw(text, replacement_dict=None):
  1016. """
  1017. Returns *text* as a string with special characters replaced by visible
  1018. equivalents using *replacement_dict*. If *replacement_dict* is None or
  1019. False the global REPLACEMENT_DICT will be used. Example::
  1020. >>> test = '\\x1b]0;Some xterm title\\x07'
  1021. >>> print(raw(test))
  1022. '^[]0;Some title^G'
  1023. """
  1024. if not replacement_dict:
  1025. replacement_dict = REPLACEMENT_DICT
  1026. out = u''
  1027. for char in text:
  1028. charnum = ord(char)
  1029. if charnum in replacement_dict.keys():
  1030. out += replacement_dict[charnum]
  1031. else:
  1032. out += char
  1033. return out
  1034. def create_data_uri(filepath, mimetype=None):
  1035. """
  1036. Given a file at *filepath*, return that file as a data URI.
  1037. Raises a `MimeTypeFail` exception if the mimetype could not be guessed.
  1038. """
  1039. import base64
  1040. if not mimetype:
  1041. mimetype = mimetypes.guess_type(filepath)[0]
  1042. if not mimetype:
  1043. raise MimeTypeFail("Could not guess mime type of: %s" % filepath)
  1044. with io.open(filepath, mode='rb') as f:
  1045. data = f.read()
  1046. encoded = base64.b64encode(data).decode('ascii').replace('\n', '')
  1047. if len(encoded) > 65000:
  1048. logging.warn(
  1049. "WARNING: Data URI > 65,000 characters. You're pushing it buddy!")
  1050. data_uri = "data:%s;base64,%s" % (mimetype, encoded)
  1051. return data_uri
  1052. def human_readable_bytes(nbytes):
  1053. """
  1054. Returns *nbytes* as a human-readable string in a similar fashion to how it
  1055. would be displayed by `ls -lh` or `df -h`.
  1056. """
  1057. K, M, G, T = 1 << 10, 1 << 20, 1 << 30, 1 << 40
  1058. if nbytes >= T:
  1059. return '%.1fT' % (float(nbytes)/T)
  1060. elif nbytes >= G:
  1061. return '%.1fG' % (float(nbytes)/G)
  1062. elif nbytes >= M:
  1063. return '%.1fM' % (float(nbytes)/M)
  1064. elif nbytes >= K:
  1065. return '%.1fK' % (float(nbytes)/K)
  1066. else:
  1067. return '%d' % nbytes
  1068. def which(binary, path=None):
  1069. """
  1070. Returns the full path of *binary* (string) just like the 'which' command.
  1071. Optionally, a *path* (colon-delimited string) may be given to use instead of
  1072. `os.environ['PATH']`.
  1073. """
  1074. if path:
  1075. paths = path.split(':')
  1076. else:
  1077. paths = os.environ['PATH'].split(':')
  1078. for path in paths:
  1079. if not os.path.exists(path):
  1080. continue
  1081. files = os.listdir(path)
  1082. if binary in files:
  1083. return os.path.join(path, binary)
  1084. return None
  1085. def touch(path):
  1086. """
  1087. Emulates the 'touch' command by creating the file at *path* if it does not
  1088. exist. If the file exist its modification time will be updated.
  1089. """
  1090. with io.open(path, 'ab'):
  1091. os.utime(path, None)
  1092. def timeout_func(func, args=(), kwargs={}, timeout_duration=10, default=None):
  1093. """
  1094. Sets a timeout on the given function, passing it the given args, kwargs,
  1095. and a *default* value to return in the event of a timeout. If *default* is
  1096. a function that function will be called in the event of a timeout.
  1097. """
  1098. import threading
  1099. class InterruptableThread(threading.Thread):
  1100. def __init__(self):
  1101. threading.Thread.__init__(self)
  1102. self.result = None
  1103. def run(self):
  1104. try:
  1105. self.result = func(*args, **kwargs)
  1106. except:
  1107. self.result = default
  1108. it = InterruptableThread()
  1109. it.start()
  1110. it.join(timeout_duration)
  1111. if it.isAlive():
  1112. if hasattr(default, '__call__'):
  1113. return default()
  1114. else:
  1115. return default
  1116. else:
  1117. return it.result
  1118. def valid_hostname(hostname, allow_underscore=False):
  1119. """
  1120. Returns True if the given *hostname* is valid according to RFC rules. Works
  1121. with Internationalized Domain Names (IDN) and optionally, hostnames with an
  1122. underscore (if *allow_underscore* is True).
  1123. The rules for hostnames:
  1124. * Must be less than 255 characters.
  1125. * Individual labels (separated by dots) must be <= 63 characters.
  1126. * Only the ASCII alphabet (A-Z) is allowed along with dashes (-) and dots (.).
  1127. * May not start with a dash or a dot.
  1128. * May not end with a dash.
  1129. * If an IDN, when converted to Punycode it must comply with the above.
  1130. IP addresses will be validated according to their well-known specifications.
  1131. Examples::
  1132. >>> valid_hostname('foo.bar.com.') # Standard FQDN
  1133. True
  1134. >>> valid_hostname('2foo') # Short hostname
  1135. True
  1136. >>> valid_hostname('-2foo') # No good: Starts with a dash
  1137. False
  1138. >>> valid_hostname('host_a') # No good: Can't have underscore
  1139. False
  1140. >>> valid_hostname('host_a', allow_underscore=True) # Now it'll validate
  1141. True
  1142. >>> valid_hostname(u'ジェーピーニック.jp') # Example valid IDN
  1143. True
  1144. """
  1145. # Convert to Punycode if an IDN
  1146. if isinstance(hostname, str):
  1147. try:
  1148. hostname = hostname.encode('idna')
  1149. except UnicodeError: # Can't convert to Punycode: Bad hostname
  1150. return False
  1151. if len(hostname) > 255:
  1152. return False
  1153. if hostname[-1:] == b".": # Strip the tailing dot if present
  1154. hostname = hostname[:-1]
  1155. allowed = re.compile(b"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
  1156. if allow_underscore:
  1157. allowed = re.compile(b"(?!-)[_A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
  1158. return all(allowed.match(x) for x in hostname.split(b"."))
  1159. def recursive_chown(path, uid, gid):
  1160. """Emulates 'chown -R *uid*:*gid* *path*' in pure Python"""
  1161. error_msg = _(
  1162. "Error: Gate One does not have the ability to recursively chown %s to "
  1163. "uid %s/gid %s. Please ensure that user, %s has write permission to "
  1164. "the directory.")
  1165. try:
  1166. os.chown(path, uid, gid)
  1167. except OSError as e:
  1168. import pwd
  1169. if e.errno in [errno.EACCES, errno.EPERM]:
  1170. raise ChownError(error_msg % (path, uid, gid,
  1171. repr(pwd.getpwuid(os.geteuid())[0])))
  1172. else:
  1173. raise
  1174. for root, dirs, files in os.walk(path):
  1175. for momo in dirs:
  1176. _path = os.path.join(root, momo)
  1177. try:
  1178. os.chown(_path, uid, gid)
  1179. except OSError as e:
  1180. import pwd
  1181. if e.errno in [errno.EACCES, errno.EPERM]:
  1182. raise ChownError(error_msg % (
  1183. _path, uid, gid, repr(pwd.getpwuid(os.geteuid())[0])))
  1184. else:
  1185. raise
  1186. for momo in files:
  1187. _path = os.path.join(root, momo)
  1188. try:
  1189. os.chown(_path, uid, gid)
  1190. except OSError as e:
  1191. import pwd
  1192. if e.errno in [errno.EACCES, errno.EPERM]:
  1193. raise ChownError(error_msg % (
  1194. _path, uid, gid, repr(pwd.getpwuid(os.geteuid())[0])))
  1195. else:
  1196. raise
  1197. def check_write_permissions(user, path):
  1198. """
  1199. Returns `True` if the given *user* has write permissions to *path*. *user*
  1200. can be a UID (int) or a username (string).
  1201. """
  1202. import pwd, grp, stat
  1203. # Get the user's complete passwd record
  1204. if isinstance(user, int):
  1205. user = pwd.getpwuid(user)
  1206. else:
  1207. user = pwd.getpwnam(user)
  1208. if user.pw_uid == 0:
  1209. return True # Assume root can write to everything (NFS notwithstanding)
  1210. groups = [] # A combination of user's primary GID and supplemental groups
  1211. for group in grp.getgrall():
  1212. if user.pw_name in group.gr_mem:
  1213. groups.append(group.gr_gid)
  1214. if group.gr_gid == user.pw_gid:
  1215. groups.append(group.gr_gid)
  1216. st = os.stat(path)
  1217. other_write = bool(st.st_mode & stat.S_IWOTH)
  1218. if other_write:
  1219. return True # Read/write world!
  1220. owner_write = bool(st.st_mode & stat.S_IWUSR)
  1221. if st.st_uid == user.pw_uid and owner_write:
  1222. return True # User can write to their own file
  1223. group_write = bool(st.st_mode & stat.S_IWGRP)
  1224. if st.st_gid in groups and group_write:
  1225. return True # User belongs to a group that can write to the file
  1226. return False
  1227. def bind(function, self):
  1228. """
  1229. Will return *function* with *self* bound as the first argument. Allows one
  1230. to write functions like this::
  1231. def foo(self, whatever):
  1232. return whatever
  1233. ...outside of the construct of a class.
  1234. """
  1235. return partial(function, self)
  1236. def minify(path_or_fileobj, kind):
  1237. """
  1238. Returns *path_or_fileobj* as a minified string. *kind* should be one of
  1239. 'js' or 'css'. Works with JavaScript and CSS files using `slimit` and
  1240. `cssmin`, respectively.
  1241. """
  1242. out = None
  1243. try:
  1244. import slimit
  1245. except ImportError:
  1246. slimit = None
  1247. try:
  1248. import cssmin
  1249. except ImportError:
  1250. cssmin = None
  1251. if isinstance(path_or_fileobj, basestring):
  1252. filename = os.path.split(path_or_fileobj)[1]
  1253. with io.open(path_or_fileobj, mode='r', encoding='utf-8') as f:
  1254. data = f.read()
  1255. else:
  1256. filename = os.path.split(path_or_fileobj.name)[1]
  1257. data = path_or_fileobj.read()
  1258. out = data
  1259. if slimit and kind == 'js':
  1260. if not filename.endswith('min.js'):
  1261. try:
  1262. out = slimit.minify(data, mangle=True)
  1263. logging.debug(_(
  1264. "(saved ~%s bytes minifying %s)" % (
  1265. (len(data) - len(out), filename)
  1266. )
  1267. ))
  1268. except Exception:
  1269. logging.error(_("slimit failed trying to minify %s") % filename)
  1270. import traceback
  1271. traceback.print_exc(file=sys.stdout)
  1272. elif cssmin and kind == 'css':
  1273. if not filename.endswith('min.css'):
  1274. out = cssmin.cssmin(data)
  1275. logging.debug(_(
  1276. "(saved ~%s bytes minifying %s)" % (
  1277. (len(data) - len(out), filename)
  1278. )
  1279. ))
  1280. return out
  1281. # This is so we can have the argument below be 'minify' (user friendly)
  1282. _minify = minify
  1283. def get_or_cache(cache_dir, path, minify=True):
  1284. """
  1285. Given a *path*, returns the cached version of that file. If the file has
  1286. yet to be cached, cache it and return the result. If *minify* is `True`
  1287. (the default), the file will be minified as part of the caching process (if
  1288. possible).
  1289. """
  1290. # Need to store the original file's modification time in the filename
  1291. # so we can tell if the original changed in the event that Gate One is
  1292. # restarted.
  1293. # Also, we're using the full path in the cached filename in the event
  1294. # that two files have the same name but at different paths.
  1295. mtime = os.stat(path).st_mtime
  1296. shortened_path = short_hash(path)
  1297. cached_filename = "%s:%s" % (shortened_path, mtime)
  1298. cached_file_path = os.path.join(cache_dir, cached_filename)
  1299. # Check if the file has changed since last time and use the cached
  1300. # version if it ma

Large files files are truncated, but you can click here to view the full file