Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

PYTHON 3 SUPPORT HAS BEEN ADDED!

All throughout the code things have been updated here and there in order to support Python 3.  A few things might be a little rough around the edges and it *does* take a long time for the 2to3 tool to work its magic when you run "python3 setup.py install" but by golly it *works*!  Not only does it work but it is FAST.  Preliminary benchmarks show Gate One using markdely less memory and CPU running under Python 3.2 (I can't wait to try 3.3!) than it does with Python 2.7.  I'll be tweaking things here and there in the next few commits (related to Python 3 support) and I wouldn't be surprised if I can make it perform even better.
setup.py:  Support for Python 3 "just works".  Though it does take a lot longer to install if you run "python3 setup.py install" (since the whole /opt/gateone dir must be walked converting everything to work with Python 3).
terminal.py:  Updated the version string to 1.1.
utils.py:  Updated the version string to 1.1.
utils.py:  Modified human_readable_bytes() to use "nbytes" as the argument instead of "bytes" to avoid the use of a python built-in.
utils.py:  Using Tornado's utf8() and to_unicode() functions in a few places to make it easier to support both python2.6+ and python3+ simultaneously.
Bookmarks Plugin:  The use of html5lib has been changed to be a dynamic import (where it's used).  I'm going to see if I can figure out a way to get importing/exporting bookmarks working without this module to make "python3 setup.py install" go faster (most of the time is spent converting html5lib!).
SSH Plugin:  ssh_connect.py:  The get_identities() function now double-checks that identities exist before it returns them as a list.  This means you won't see them when ssh_connect.py prints that "The following identities will be used..." message (which is good and logical).  The next time the user marks an identity as default the .default_ids file will get cleaned up automatically.
  • Loading branch information...
commit 2aecefaeb40e0fbc9185bcb6f3e6f86363390612 1 parent 29e6e44
@liftoff authored
View
4 gateone/auth.py
@@ -93,7 +93,7 @@ def user_login(self, user):
if not os.path.exists(user_dir):
logging.info(_("Creating user directory: %s" % user_dir))
mkdir_p(user_dir)
- os.chmod(user_dir, 0700)
+ os.chmod(user_dir, 0o700)
session_file = os.path.join(user_dir, 'session')
session_file_exists = os.path.exists(session_file)
if session_file_exists:
@@ -174,7 +174,7 @@ def user_login(self, user):
if not os.path.exists(user_dir):
logging.info(_("Creating user directory: %s" % user_dir))
mkdir_p(user_dir)
- os.chmod(user_dir, 0700)
+ os.chmod(user_dir, 0o700)
session_info = {
'upn': user,
'session': generate_session_id()
View
2  gateone/authpam.py
@@ -78,7 +78,7 @@ def _pam_conv(auth, query_list, user_data=None):
try:
pam_auth.authenticate()
pam_auth.acct_mgmt()
- except Exception, e: # Basic auth failed
+ except Exception as e: # Basic auth failed
if self.settings['debug']:
print(e) # Very useful for debugging Kerberos errors
return self.authenticate_redirect()
View
32 gateone/gateone.py
@@ -886,8 +886,7 @@ def on_message(self, message):
# Try, try again
self.commands[key]()
except (KeyError, TypeError, AttributeError) as e:
- print('Got error2? %s' % e)
- pass # Ignore commands we don't understand
+ logging.error(_('Unknown WebSocket action: %s' % key))
def on_close(self):
"""
@@ -1049,7 +1048,7 @@ def authenticate(self, settings):
logging.info(
_("Creating user directory: %s" % user_dir))
mkdir_p(user_dir)
- os.chmod(user_dir, 0770)
+ os.chmod(user_dir, 0o770)
session_file = os.path.join(user_dir, 'session')
if os.path.exists(session_file):
session_data = open(session_file).read()
@@ -1114,7 +1113,7 @@ def authenticate(self, settings):
session_dir = os.path.join(session_dir, self.session)
if not os.path.exists(session_dir):
mkdir_p(session_dir)
- os.chmod(session_dir, 0770)
+ os.chmod(session_dir, 0o770)
for item in os.listdir(session_dir):
if item.startswith('dtach_'):
term = int(item.split('_')[1])
@@ -1229,7 +1228,7 @@ def new_terminal(self, settings):
# Create the session dir if not already present
if not os.path.exists(session_dir):
mkdir_p(session_dir)
- os.chmod(session_dir, 0770)
+ os.chmod(session_dir, 0o770)
if self.settings['dtach']: # Wrap in dtach (love this tool!)
dtach_path = "%s/dtach_%s" % (session_dir, term)
if os.path.exists(dtach_path):
@@ -1572,7 +1571,7 @@ def resize(self, resize_obj):
ctrl_l=ctrl_l
)
else: # Resize them all
- for term in SESSIONS[self.session].keys():
+ for term in list(SESSIONS[self.session].keys()):
if isinstance(term, int): # Skip the TidyThread
SESSIONS[self.session][term]['multiplex'].resize(
self.rows,
@@ -1585,8 +1584,6 @@ def resize(self, resize_obj):
@require_auth
def char_handler(self, chars):
"""Writes *chars* (string) to the currently-selected terminal"""
- if type(chars) != unicode:
- chars = unicode(chars)
term = self.current_term
session = self.session
if session in SESSIONS and term in SESSIONS[session]:
@@ -1717,7 +1714,7 @@ def get_style(self, settings):
if os.path.isdir(os.path.join(plugins_dir, f)):
out_dict['plugins'][f] = ''
# Add each plugin's CSS template(s) to its respective dict
- for plugin in out_dict['plugins'].keys():
+ for plugin in list(out_dict['plugins'].keys()):
plugin_templates_path = os.path.join(
plugins_dir, plugin, 'templates')
if os.path.exists(plugin_templates_path):
@@ -1731,8 +1728,10 @@ def get_style(self, settings):
prefix=prefix,
url_prefix=self.settings['url_prefix']
)
+ if isinstance(plugin_css, bytes): # Python3
+ plugin_css = str(plugin_css, 'UTF-8')
out_dict['plugins'][plugin] += plugin_css
- self.write_message({'load_style': out_dict})
+ self.write_message(json_encode({'load_style': out_dict}))
# NOTE: This has been disabled for now. It works OK but the problem is that
# some plugin JS needs to load *before* the WebSocket is connected. Might
@@ -1867,7 +1866,7 @@ def run(self):
break
# This loops through all the open terminals checking if each is alive
all_dead = True
- for term in SESSIONS[session].keys():
+ for term in list(SESSIONS[session].keys()):
if isinstance(term, int) and term in SESSIONS[session]:
if SESSIONS[session][term]['multiplex'].isalive():
all_dead = False
@@ -1897,7 +1896,7 @@ def run(self):
"Killing termio session.".format(session=self.session)
))
# Clean up:
- for term in SESSIONS[session].keys():
+ for term in list(SESSIONS[session].keys()):
try:
if SESSIONS[session][term]['multiplex'].isalive():
SESSIONS[session][term]['multiplex'].terminate()
@@ -2062,7 +2061,7 @@ def main():
if PAMAuthHandler:
auths += ", pam"
# Simplify the syslog_facility option help message
- facilities = FACILITIES.keys()
+ facilities = list(FACILITIES.keys())
facilities.sort()
# Figure out the default origins
default_origins = [
@@ -2260,7 +2259,8 @@ def main():
default=default_locale,
help=_("The locale (e.g. pt_PT) Gate One should use for translations."
" If not provided, will default to $LANG (which is '%s' in your "
- "current shell), or en_US if not set.") % os.environ.get('LANG', 'not set').split('.')[0],
+ "current shell), or en_US if not set."
+ % os.environ.get('LANG', 'not set').split('.')[0]),
type=str
)
define("js_init",
@@ -2390,7 +2390,7 @@ def main():
repr(os.getlogin()), repr(os.getlogin()))))
sys.exit(1)
# If we could create it we should be able to adjust its permissions:
- os.chmod(options.user_dir, 0770)
+ os.chmod(options.user_dir, 0o770)
if os.stat(options.user_dir).st_uid != options.uid:
# Try correcting this first
try:
@@ -2408,7 +2408,7 @@ def main():
"yourself and make user, %s its owner." % (options.session_dir,
repr(os.getlogin()), repr(os.getlogin()))))
sys.exit(1)
- os.chmod(options.session_dir, 0770)
+ os.chmod(options.session_dir, 0o770)
if os.stat(options.session_dir).st_uid != options.uid:
# Try correcting it
try:
View
15 gateone/plugins/bookmarks/bookmarks.py
@@ -8,7 +8,7 @@
"""
# Meta
-__version__ = '1.0rc1'
+__version__ = '1.0'
__license__ = "GNU AGPLv3 or Proprietary (see LICENSE.txt)"
__version_info__ = (1, 0)
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
@@ -29,8 +29,6 @@
# The following two lines let us import modules in the "dependencies" dir
plugin_path = os.path.split(__file__)[0]
sys.path.append(os.path.join(plugin_path, "dependencies"))
-import html5lib
-from html5lib import treebuilders, treewalkers
# Globals
boolean_fix = {
@@ -61,10 +59,11 @@ def parse_bookmarks_html(html):
# If this looks impossibly complicated it's because parsing HTML streams is
# dark voodoo. I had to push my brains back behind my eyes and into my ears
# a few times while writing this.
+ import html5lib
out_list = []
- p = html5lib.HTMLParser(tree=treebuilders.getTreeBuilder("dom"))
+ p = html5lib.HTMLParser(tree=html5lib.treebuilders.getTreeBuilder("dom"))
dom_tree = p.parse(html)
- walker = treewalkers.getTreeWalker("dom")
+ walker = html5lib.treewalkers.getTreeWalker("dom")
stream = walker(dom_tree)
level = 0
tags = []
@@ -442,9 +441,11 @@ def get_favicon_url(self, html):
If no favicon can be found, returns:
(None, None)
"""
- p = html5lib.HTMLParser(tree=treebuilders.getTreeBuilder("dom"))
+ import html5lib
+ p = html5lib.HTMLParser(
+ tree=html5lib.treebuilders.getTreeBuilder("dom"))
dom_tree = p.parse(html)
- walker = treewalkers.getTreeWalker("dom")
+ walker = html5lib.treewalkers.getTreeWalker("dom")
stream = walker(dom_tree)
fetch_url = None
mimetype = None
View
21 gateone/plugins/ssh/scripts/ssh_connect.py
@@ -78,8 +78,10 @@ def short_hash(to_shorten):
Converts *to_shorten* into a really short hash depenendent on the length of
*to_shorten*. The result will be safe for use as a file name.
"""
- packed = struct.pack('i', binascii.crc32(to_shorten))
- return base64.urlsafe_b64encode(packed).replace('=', '')
+ if bytes != str: # Python 3
+ to_shorten = bytes(to_shorten, 'UTF-8')
+ packed = struct.pack('I', binascii.crc32(to_shorten))
+ return str(base64.urlsafe_b64encode(packed)).replace('=', '')
def valid_hostname(hostname, allow_underscore=False):
"""
@@ -118,6 +120,10 @@ def valid_hostname(hostname, allow_underscore=False):
hostname = hostname.encode('idna')
except UnicodeError: # Can't convert to Punycode: Bad hostname
return False
+ try:
+ hostname = str(hostname, 'UTF-8')
+ except TypeError: # Python 2.6+. Just ignore
+ pass
if len(hostname) > 255:
return False
if hostname[-1:] == ".": # Strip the tailing dot if present
@@ -162,7 +168,8 @@ def get_identities(users_ssh_dir, only_defaults=False):
with open(os.path.join(users_ssh_dir, '.default_ids')) as f:
defaults = f.read().splitlines()
# Fix empty entries
- defaults = [a for a in defaults if a]
+ defaults = [a for a in defaults if os.path.exists(
+ os.path.join(users_ssh_dir, a))]
# Reduce absolute paths to short names (for easy matching)
defaults = [os.path.split(a)[1] for a in defaults]
for f in ssh_files:
@@ -335,7 +342,7 @@ def openssh_connect(
# Also make sure the base directory exists
basedir = os.path.split(socket)[0]
mkdir_p(basedir)
- os.chmod(basedir, 0700) # 0700 for good security practices
+ os.chmod(basedir, 0o700) # 0700 for good security practices
args.insert(1, socket_arg) # After -M so it is easier to see in ps
if additional_args:
if isinstance(additional_args, list):
@@ -347,7 +354,7 @@ def openssh_connect(
if password:
# Create a temporary script to use with SSH_ASKPASS
temp = tempfile.NamedTemporaryFile(delete=False)
- os.chmod(temp.name, 0700)
+ os.chmod(temp.name, 0o700)
temp.write('#!/bin/bash\necho "%s"\n' % password)
temp.close()
env['SSH_ASKPASS'] = temp.name
@@ -380,7 +387,7 @@ def openssh_connect(
# NOTE: We wrap in a shell script so we can execute it and immediately quit.
# By doing this instead of keeping ssh_connect.py running we can save a lot
# of memory (depending on how many terminals are open).
- os.chmod(script_path, 0700) # 0700 for good security practices
+ os.chmod(script_path, 0o700) # 0700 for good security practices
if password:
# SSH_ASKPASS needs some special handling
pid = os.fork()
@@ -456,7 +463,7 @@ def telnet_connect(user, host, port=23, env=None):
# NOTE: We wrap in a shell script so we can execute it and immediately quit.
# By doing this instead of keeping ssh_connect.py running we can save a lot
# of memory (depending on how many terminals are open).
- os.chmod(script_path, 0700) # 0700 for good security practices
+ os.chmod(script_path, 0o700) # 0700 for good security practices
os.execvpe(script_path, [], env)
os._exit(0)
View
14 gateone/terminal.py
@@ -4,9 +4,9 @@
#
# Meta
-__version__ = '1.0'
+__version__ = '1.1'
__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
-__version_info__ = (1, 0)
+__version_info__ = (1, 1)
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
__doc__ = """\
@@ -1225,13 +1225,13 @@ def write(self, chars, special_checks=True):
cursor_right = self.cursor_right
magic = self.magic
changed = False
- #logging.debug('handling chars: %s' % `chars`)
+ #logging.debug('handling chars: %s' % repr(chars))
if special_checks:
# NOTE: Special checks are limited to PNGs and JPEGs right now
before_chars = ""
after_chars = ""
for magic_header in magic.keys():
- if magic_header.match(chars):
+ if magic_header.match(str(chars)):
self.matched_header = magic_header
self.timeout_image = datetime.now()
if self.image or self.matched_header:
@@ -1261,11 +1261,11 @@ def write(self, chars, special_checks=True):
return
# Have to convert to unicode
try:
- chars = unicode(chars.decode('utf-8', "handle_special"))
+ chars = chars.decode('utf-8', "handle_special")
except UnicodeEncodeError:
# Just in case
try:
- chars = unicode(chars.decode('utf-8', "ignore"))
+ chars = chars.decode('utf-8', "ignore")
except UnicodeEncodeError:
logging.error(
_("Double UnicodeEncodeError in Terminal.terminal."))
@@ -1644,7 +1644,7 @@ def _capture_image(self):
"""
logging.debug("_capture_image() len(self.image): %s" % len(self.image))
# Remove the extra \r's that the terminal adds:
- self.image = self.image.replace('\r\n', '\n')
+ self.image = str(self.image).replace('\r\n', '\n')
if Image: # PIL is loaded--try to guess how many lines the image takes
i = StringIO.StringIO(self.image)
try:
View
2  gateone/termio.py
@@ -1239,7 +1239,7 @@ def _read(self, bytes=-1):
# eventually have to process it all which would
# take forever.
break
- result += updated
+ result += str(updated)
timeout_func(
self.term_write,
args=(updated,),
View
65 gateone/utils.py
@@ -9,9 +9,9 @@
"""
# Meta
-__version__ = '1.0'
+__version__ = '1.1'
__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
-__version_info__ = (1, 0)
+__version_info__ = (1, 1)
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
# Import stdlib stuff
@@ -21,12 +21,9 @@
import random
import re
import errno
-import base64
import uuid
import logging
import mimetypes
-import struct
-import binascii
import gzip
import fcntl
from datetime import timedelta
@@ -39,6 +36,7 @@
except ImportError: # Tornado isn't available
from json import dumps as _json_encode
from json import loads as json_encode
+from tornado.escape import to_unicode, utf8
# Globals
MACOS = os.uname()[0] == 'Darwin'
@@ -146,12 +144,12 @@ def write_pid(path):
"""Writes our PID to *path*."""
try:
pid = os.getpid()
- pidfile = open(path, 'wb')
- # Get a non-blocking exclusive lock
- fcntl.flock(pidfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
- pidfile.seek(0)
- pidfile.truncate(0)
- pidfile.write(str(pid))
+ with open(path, 'w') as pidfile:
+ # Get a non-blocking exclusive lock
+ fcntl.flock(pidfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
+ pidfile.seek(0)
+ pidfile.truncate(0)
+ pidfile.write(str(pid))
except:
raise
finally:
@@ -217,7 +215,7 @@ def json_encode(obj):
doesn't seem to work just right when it comes to returning unicode strings.
This is just a wrapper that ensures that the returned string is unicode.
"""
- return unicode(_json_encode(obj))
+ return to_unicode(_json_encode(obj))
def get_translation():
"""
@@ -416,7 +414,12 @@ def generate_session_id():
"NzY4YzFmNDdhMTM1NDg3Y2FkZmZkMWJmYjYzNjBjM2Y5O"
>>>
"""
- return base64.b64encode(uuid.uuid4().hex + uuid.uuid4().hex)[:45]
+ import base64
+ session_id = base64.b64encode(
+ utf8(uuid.uuid4().hex + uuid.uuid4().hex))[:45]
+ if isinstance(session_id, bytes): # Python3
+ return str(session_id, 'UTF-8')
+ return session_id
def mkdir_p(path):
"""
@@ -474,8 +477,9 @@ def short_hash(to_shorten):
Converts *to_shorten* into a really short hash depenendent on the length of
*to_shorten*. The result will be safe for use as a file name.
"""
- packed = struct.pack('i', binascii.crc32(to_shorten))
- return base64.urlsafe_b64encode(packed).replace('=', '')
+ import struct, binascii, base64
+ packed = struct.pack('I', binascii.crc32(utf8(to_shorten)))
+ return str(base64.urlsafe_b64encode(packed)).replace('=', '')
def get_process_tree(parent_pid):
"""
@@ -875,6 +879,7 @@ def create_data_uri(filepath):
Raises a `MimeTypeFail` exception if the mimetype could not be guessed.
"""
+ import base64
mimetype = mimetypes.guess_type(filepath)[0]
if not mimetype:
raise MimeTypeFail("Could not guess mime type of: %s" % filepath)
@@ -887,22 +892,22 @@ def create_data_uri(filepath):
data_uri = "data:%s;base64,%s" % (mimetype, encoded)
return data_uri
-def human_readable_bytes(bytes):
+def human_readable_bytes(nbytes):
"""
- Returns *bytes* as a human-readable string in a similar fashion to how it
+ Returns *nbytes* as a human-readable string in a similar fashion to how it
would be displayed by 'ls -lh' or 'df -h'.
"""
K, M, G, T = 1 << 10, 1 << 20, 1 << 30, 1 << 40
- if bytes >= T:
- return '%.1fT' % (float(bytes)/T)
- elif bytes >= G:
- return '%.1fG' % (float(bytes)/G)
- elif bytes >= M:
- return '%.1fM' % (float(bytes)/M)
- elif bytes >= K:
- return '%.1fK' % (float(bytes)/K)
+ if nbytes >= T:
+ return '%.1fT' % (float(nbytes)/T)
+ elif nbytes >= G:
+ return '%.1fG' % (float(nbytes)/G)
+ elif nbytes >= M:
+ return '%.1fM' % (float(nbytes)/M)
+ elif nbytes >= K:
+ return '%.1fK' % (float(nbytes)/K)
else:
- return '%d' % bytes
+ return '%d' % nbytes
def retrieve_first_frame(golog_path):
"""
@@ -1161,22 +1166,22 @@ def drop_privileges(uid='nobody', gid='nogroup', supl_groups=None):
supl_groups[i] = grp.getgrnam(group).gr_gid
try:
os.setgroups(supl_groups)
- except OSError, e:
+ except OSError as e:
logging.error(_('Could not set supplemental groups: %s' % e))
exit()
# Try setting the new uid/gid
try:
os.setgid(running_gid)
- except OSError, e:
+ except OSError as e:
logging.error(_('Could not set effective group id: %s' % e))
exit()
try:
os.setuid(running_uid)
- except OSError, e:
+ except OSError as e:
logging.error(_('Could not set effective user id: %s' % e))
exit()
# Ensure a very convervative umask
- new_umask = 077
+ new_umask = 0o77
old_umask = os.umask(new_umask)
final_uid = os.getuid()
final_gid = os.getgid()
View
45 setup.py
@@ -6,9 +6,20 @@
# Globals
POSIX = 'posix' in sys.builtin_module_names
-version = '1.0'
-# Some paths we can reference
+version = '1.1'
setup_dir = os.path.dirname(os.path.abspath(__file__))
+major, minor = sys.version_info[:2] # Python version
+if major == 2 and minor <=5:
+ print("Gate One requires Python 2.6+. You are running %s" % sys.version)
+ sys.exit(1)
+if major == 3:
+ try:
+ import lib2to3 # Just a check--the module is not actually used
+ except ImportError:
+ print("Python 3.X support requires the 2to3 tool.")
+ sys.exit(1)
+
+# Some paths we can reference
static_dir = os.path.join(setup_dir, 'gateone', 'static')
plugins_dir = os.path.join(setup_dir, 'gateone', 'plugins')
templates_dir = os.path.join(setup_dir, 'gateone', 'templates')
@@ -117,5 +128,31 @@ def walk_data_files(path, install_path=prefix):
author_email = 'daniel.mcdougall@liftoffsoftware.com',
requires=["tornado (>=2.2)"],
provides = ['gateone'],
- data_files = data_files,
-)
+ data_files = data_files
+)
+
+# Python3 support stuff is below
+def fix_shebang(filepath):
+ """
+ Swaps 'python' for 'python3' in the shebang (if present) in the given
+ *filepath*.
+ """
+ contents = []
+ with open(filepath, 'r') as f:
+ contents = f.readlines()
+ if contents and contents[0].startswith('#!'): # Shebang
+ with open(filepath, 'w') as f:
+ contents[0] = contents[0].replace('python', 'python3')
+ f.write("".join(contents))
+
+if major == 3:
+ from subprocess import getstatusoutput
+ command = "2to3 -w -n -x print -x dict -x input %s"
+ for (dirpath, dirs, filenames) in os.walk(os.path.join(prefix, 'gateone')):
+ for f in filenames:
+ if f.endswith('.py'):
+ filepath = os.path.join(dirpath, f)
+ print("Converting to python3: %s" % filepath)
+ # Fix the shebang if present
+ fix_shebang(filepath)
+ retcode, output = getstatusoutput(command % filepath)
Please sign in to comment.
Something went wrong with that request. Please try again.