Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added a new module: gopam.py. It is a modified version of the pam-0.1…

….3 module by Chris AtLee <chris@atlee.ca> that adds support for setting PAM_TTY. Why not continue using PyPAM? PyPAM is not portable... You get it in Linux distributions based on Debian and Red Hat but nothing else. This module should work on any platform that uses PAM. Also, PyPAM appears to be abandoned--the original download URL is inaccessible. All we have left is the source packages from Debian and Red Hat. One less thing to install too.

authpam.py:  Modified to use the new (much simpler) gopam.py instead of PyPAM (aka "PAM" in all caps).
auth.py:  Added some information about the APIAuthHandler to the docstring.
LICENSE.txt:  Added information about the gopam.py module being MIT licensed.
gateone.js:  fixed a bug with the recent change I made that sets the selectedTerminal in localStorage (was missing an underscore).
SSH Plugin:  From now on user's SSH directories will start with a dot (.) to better emulate how OpenSSH works.  This will also allow you to use a regular home directory without having to worry about keeping 'ssh' in sync with '.ssh'.  The SSH plugin will automatically rename existing directories to .ssh (if a directory with that name doesn't already exist--don't worry, it's safe :).
gateone.py:  Added a new plugin hook:  'Command'.  It gives plugins the ability to transform the configured 'command' (e.g. in server.conf) before it is executed.  So you can make your own replacement variables (like %USER%) if you like.
Example plugin:  Added an example of the new 'Command' hook.
  • Loading branch information...
commit adfc6c11f38625db16b0e3f9a8a88f6a7d7a009a 1 parent 60b338e
@liftoff authored
View
4 LICENSE.txt
@@ -32,6 +32,10 @@ under the "Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)" license:
http://creativecommons.org/licenses/by-sa/3.0/
+The gopam.py module is licensed under the MIT license:
+
+ http://www.opensource.org/licenses/mit-license.php
+
Portional items
^^^^^^^^^^^^^^^
Portions of Gate One's JavaScript contain code taken from the excellent MochiKit
View
8 gateone/auth.py
@@ -20,6 +20,7 @@
--auth=kerberos KerberosAuthHandler
--auth=google GoogleAuthHandler
--auth=pam PAMAuthHandler
+--auth=api APIAuthHandler
=============== ===================
.. note:: API authentication is handled inside of :ref:`gateone.py`
@@ -54,6 +55,13 @@
.. note:: This authentication type is perfect if you're using Chromebooks (Chrome OS devices).
+API Authentication
+------------------
+API-based authentication is actually handled in gateone.py but we still need
+*something* to exist at the /auth URL that will always return the
+'unauthenticated' response. This ensures that no one can authenticate
+themselves by visiting that URL manually.
+
Docstrings
==========
"""
View
37 gateone/authpam.py
@@ -9,9 +9,8 @@
__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
__version_info__ = (1, 0)
__doc__ = """
-This authentication module is built on top of python-pam (or PAM). The latest
-version of which can be found here: ftp://ftp.pangalactic.org/pub/tummy/ or if
-that doesn't work try: http://packages.debian.org/lenny/python-pam
+This authentication module is built on top of gopam.py which is included with
+Gate One.
It was originally written by Alan Schmitz.
@@ -24,8 +23,10 @@
# Standard library modules
import base64
+# Our modules
+import gopam
+
# 3rd party modules
-import PAM
import tornado.httpserver
import tornado.ioloop
import tornado.web
@@ -57,27 +58,15 @@ def auth_basic(self, auth_header, callback):
"""
auth_decoded = base64.decodestring(auth_header[6:])
username, password = auth_decoded.split(':', 1)
-
- def _pam_conv(auth, query_list, user_data=None):
- resp = []
- for i in range(len(query_list)):
- query, qtype = query_list[i]
- if qtype == PAM.PAM_PROMPT_ECHO_ON:
- resp.append((username, 0))
- elif qtype == PAM.PAM_PROMPT_ECHO_OFF:
- resp.append((password, 0))
- else:
- return None
- return resp
-
- pam_auth = PAM.pam()
- pam_auth.start(self.settings['pam_service'])
- pam_auth.set_item(PAM.PAM_USER, username)
- pam_auth.set_item(PAM.PAM_TTY, 'console')
- pam_auth.set_item(PAM.PAM_CONV, _pam_conv)
try:
- pam_auth.authenticate()
- pam_auth.acct_mgmt()
+ result = gopam.authenticate(
+ username,
+ password,
+ service=self.settings['pam_service'],
+ tty="console",
+ PAM_RHOST=self.request.remote_ip) # RHOST so it shows up in logs
+ if not result:
+ return self.authenticate_redirect()
except Exception as e: # Basic auth failed
if self.settings['debug']:
logging.debug(e)
View
24 gateone/gateone.py
@@ -349,6 +349,11 @@ def _(string):
# with 'self' and the new instance of the terminal emulator as the only
# arguments. It's a more DIY/generic version of PLUGIN_TERM_HOOKS.
PLUGIN_NEW_TERM_HOOKS = []
+# 'Command' hooks get called before a new Multiplex instance is created inside
+# of TerminalWebSocket.new_multiplex(). They are passed the 'command' and must
+# return a string that will be used as the replacement 'command'. This allows
+# plugin authors to modify the configured 'command' before it is executed
+PLUGIN_COMMAND_HOOKS = []
# 'Multiplex' hooks get called at the end of TerminalWebSocket.new_multiplex()
# with the instance of TerminalWebSocket and the new instance of Multiplex as
# the only arguments, respectively.
@@ -983,9 +988,10 @@ class TerminalWebSocket(WebSocketHandler):
WebSocket.
"""
instances = set()
+ commands = {}
def __init__(self, application, request):
WebSocketHandler.__init__(self, application, request)
- self.commands = {
+ self.commands.update({
'ping': self.pong,
'authenticate': self.authenticate,
'new_terminal': self.new_terminal,
@@ -1005,7 +1011,7 @@ def __init__(self, application, request):
'manual_title': self.manual_title,
'reset_terminal': self.reset_terminal,
'debug_terminal': self.debug_terminal
- }
+ })
self.terms = {}
# So we can keep track and avoid sending unnecessary messages:
self.titles = {}
@@ -1435,6 +1441,9 @@ def authenticate(self, settings):
SESSIONS[self.session]['last_seen'] = 'connected'
if self.location not in SESSIONS[self.session]:
SESSIONS[self.session][self.location] = {}
+ # TODO: Take the terminal-specific stuff out of this function
+ # ...need an 'application hooks' call here that would execute the
+ # terminal-specific stuff.
terminals = []
for term in list(SESSIONS[self.session][self.location].keys()):
if isinstance(term, int): # Only terminals are integers in the dict
@@ -1498,6 +1507,10 @@ def new_multiplex(self, cmd, term_id, logging=True):
log_name = datetime.now().strftime('%Y%m%d%H%M%S%f.golog')
log_path = os.path.join(log_dir, log_name)
facility = string_to_syslog_facility(self.settings['syslog_facility'])
+ # This allows plugins to transform the command however they like
+ if PLUGIN_COMMAND_HOOKS:
+ for func in PLUGIN_COMMAND_HOOKS:
+ cmd = func(cmd)
m = termio.Multiplex(
cmd,
log_path=log_path,
@@ -2413,6 +2426,7 @@ def __init__(self, settings):
in the Tornado settings dict so as to be accessible under self.settings.
"""
global PLUGIN_WS_CMDS
+ global PLUGIN_COMMAND_HOOKS
global PLUGIN_HOOKS
global PLUGIN_ESC_HANDLERS
global PLUGIN_AUTH_HOOKS
@@ -2513,6 +2527,12 @@ def __init__(self, settings):
PLUGIN_AUTH_HOOKS.extend(hooks['Auth'])
else:
PLUGIN_AUTH_HOOKS.append(hooks['Auth'])
+ if 'Command' in hooks:
+ # Apply the plugin's 'Command' hooks (called by new_multiplex)
+ if isinstance(hooks['Command'], (list, tuple)):
+ PLUGIN_COMMAND_HOOKS.extend(hooks['Command'])
+ else:
+ PLUGIN_COMMAND_HOOKS.append(hooks['Command'])
if 'Multiplex' in hooks:
# Apply the plugin's Multiplex hooks (called by new_multiplex)
if isinstance(hooks['Multiplex'], (list, tuple)):
View
160 gateone/gopam.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# Original version (pam-0.1.3) ©2007 Chris AtLee <chris@atlee.ca>
+# This version (modifications) © 2012 Liftoff Software Corporation
+# Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+# This is a modified version of pam-0.1.3 that adds support for
+# pam_set_item (specificallly, to support setting a PAM_TTY)
+
+# Meta
+__version__ = '1.0'
+__license__ = "MIT"
+__version_info__ = (1.0)
+__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
+
+__doc__ = """\
+PAM Authentication Module for Python
+====================================
+Provides an authenticate function that will allow the caller to authenticate
+a user against the Pluggable Authentication Modules (PAM) on the system.
+
+Implemented using ctypes, so no compilation is necessary.
+"""
+
+__all__ = ['authenticate']
+
+from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof
+from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int
+from ctypes.util import find_library
+
+LIBPAM = CDLL(find_library("pam"))
+LIBC = CDLL(find_library("c"))
+
+CALLOC = LIBC.calloc
+CALLOC.restype = c_void_p
+CALLOC.argtypes = [c_uint, c_uint]
+
+STRDUP = LIBC.strdup
+STRDUP.argstypes = [c_char_p]
+STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!!
+
+# Various constants
+PAM_PROMPT_ECHO_OFF = 1
+PAM_PROMPT_ECHO_ON = 2
+PAM_ERROR_MSG = 3
+PAM_TEXT_INFO = 4
+# pam_set_item and pam_get_item constants:
+PAM_SERVICE = 1 # The service name
+PAM_USER = 2 # The user name
+PAM_TTY = 3 # The tty name
+PAM_RHOST = 4 # The remote host name
+PAM_CONV = 5 # The pam_conv structure
+PAM_AUTHTOK = 6 # The authentication token (password)
+PAM_OLDAUTHTOK = 7 # The old authentication token
+PAM_RUSER = 8 # The remote user name
+PAM_USER_PROMPT = 9 # the prompt for getting a username
+# These are Linux-specific pam_set_item/pam_get_item constants:
+PAM_FAIL_DELAY = 10 # app supplied function to override failure
+PAM_XDISPLAY = 11 # X display name
+PAM_XAUTHDATA = 12 # X server authentication data
+PAM_AUTHTOK_TYPE = 13 # The type for pam_get_authtok
+
+class PamHandle(Structure):
+ """wrapper class for pam_handle_t"""
+ _fields_ = [("handle", c_void_p)]
+
+ def __init__(self):
+ Structure.__init__(self)
+ self.handle = 0
+
+class PamMessage(Structure):
+ """wrapper class for pam_message structure"""
+ _fields_ = [("msg_style", c_int), ("msg", c_char_p)]
+
+ def __repr__(self):
+ return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
+
+class PamResponse(Structure):
+ """wrapper class for pam_response structure"""
+ _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
+
+ def __repr__(self):
+ return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
+
+CONV_FUNC = CFUNCTYPE(
+ c_int,
+ c_int,
+ POINTER(POINTER(PamMessage)),
+ POINTER(POINTER(PamResponse)),
+ c_void_p)
+
+class PamConv(Structure):
+ """wrapper class for pam_conv structure"""
+ _fields_ = [("conv", CONV_FUNC), ("appdata_ptr", c_void_p)]
+
+PAM_START = LIBPAM.pam_start
+PAM_START.restype = c_int
+PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
+
+PAM_AUTHENTICATE = LIBPAM.pam_authenticate
+PAM_AUTHENTICATE.restype = c_int
+PAM_AUTHENTICATE.argtypes = [PamHandle, c_int]
+
+PAM_SET_ITEM = LIBPAM.pam_set_item
+PAM_SET_ITEM.restype = c_int
+PAM_SET_ITEM.argtypes = [PamHandle, c_int, c_char_p]
+
+def authenticate(username, password, service='login', tty="console", **kwargs):
+ """
+ Returns True if the given username and password authenticate for the
+ given service. Returns False otherwise.
+
+ :param string username: The username to authenticate.
+ :param string password: The password in plain text.
+ :param string service: The PAM service to authenticate against. Defaults to 'login'.
+ :param string tty: Name of the TTY device to use when authenticating. Defaults to 'console' (to allow root).
+
+ If additional keyword arguments are provided they will be passed to
+ PAM_SET_ITEM() like so::
+
+ PAM_SET_ITEM(handle, <keyword mapped to PAM_whatever>, <value>)
+
+ Where the keyword will be automatically converted to a PAM_whatever constant
+ if present in this file. Example::
+
+ authenticate(user, pass, PAM_RHOST="myhost")
+
+ ...would result in::
+
+ PAM_SET_ITEM(handle, 4, "myhost") # PAM_RHOST (4) taken from the global
+ """
+ @CONV_FUNC
+ def my_conv(n_messages, messages, p_response, app_data):
+ """Simple conversation function that responds to any
+ prompt where the echo is off with the supplied password"""
+ # Create an array of n_messages response objects
+ addr = CALLOC(n_messages, sizeof(PamResponse))
+ p_response[0] = cast(addr, POINTER(PamResponse))
+ for i in range(n_messages):
+ if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
+ pw_copy = STRDUP(str(password))
+ p_response.contents[i].resp = cast(pw_copy, c_char_p)
+ p_response.contents[i].resp_retcode = 0
+ return 0
+ handle = PamHandle()
+ conv = PamConv(my_conv, 0)
+ retval = PAM_START(service, username, pointer(conv), pointer(handle))
+ PAM_SET_ITEM(handle, PAM_TTY, tty)
+ for key, value in kwargs.items():
+ if key.startswith('PAM_') and key in globals():
+ PAM_SET_ITEM(handle, globals()[key], value)
+ if retval != 0:
+ # TODO: This is not an authentication error, something
+ # has gone wrong starting up PAM
+ return False
+ retval = PAM_AUTHENTICATE(handle, 0)
+ return retval == 0
+
+if __name__ == "__main__":
+ import getpass
+ print authenticate(getpass.getuser(), getpass.getpass())
View
11 gateone/plugins/example/example.py
@@ -211,6 +211,16 @@ def example_opt_esc_handler(message, tws):
"You just executed the Example plugin's optional escape sequence handler!"}
tws.write_message(message)
+def example_command_hook(command):
+ """
+ This demonstrates how to modify Gate One's configured 'command' before it is
+ executed. It will replace any occurrance of %EXAMPLE% with 'foo'. So if
+ 'command = "some_script.sh %EXAMPLE%"' in your server.conf it would be
+ transformed to "some_script.sh foo" before being executed when a user opens
+ a new terminal.
+ """
+ return command.replace(r'%EXAMPLE%', 'foo')
+
# SOESH allows plugins to attach actions that will be called whenever a terminal
# encounters the
@@ -221,6 +231,7 @@ def example_opt_esc_handler(message, tws):
'WebSocket': {
'example_action': example_websocket_action
},
+ 'Command': example_command_hook,
'Escape': example_opt_esc_handler,
'Environment': {
'EXAMPLE_VAR': 'This was set via the Example plugin'
View
12 gateone/plugins/ssh/ssh.py
@@ -110,10 +110,20 @@ def get_ssh_dir(tws):
"""
Given a :class:`gateone.TerminalWebSocket` (*tws*) instance, return the
current user's ssh directory
+
+ .. note:: If the user's ssh directory doesn't start with a . (dot) it will be renamed.
"""
user = tws.get_current_user()['upn']
users_dir = os.path.join(tws.settings['user_dir'], user) # "User's dir"
- users_ssh_dir = os.path.join(users_dir, 'ssh')
+ old_ssh_dir = os.path.join(users_dir, 'ssh')
+ users_ssh_dir = os.path.join(users_dir, '.ssh')
+ if os.path.exists(old_ssh_dir):
+ if not os.path.exists(users_ssh_dir):
+ os.rename(old_ssh_dir, users_ssh_dir)
+ else:
+ logging.warning(
+ "Both an 'ssh' and '.ssh' directory exist for user %s. "
+ "Using the .ssh directory.")
return users_ssh_dir
def open_sub_channel(term, tws):
View
4 gateone/static/gateone.js
@@ -761,8 +761,8 @@ var go = GateOne.Base.update(GateOne, {
});
// Apply some universal defaults
-if (!localStorage[GateOne.prefs.prefix+GateOne.location+'selectedTerminal']) {
- localStorage[GateOne.prefs.prefix+GateOne.location+'selectedTerminal'] = 1;
+if (!localStorage[GateOne.prefs.prefix+GateOne.location+'_selectedTerminal']) {
+ localStorage[GateOne.prefs.prefix+GateOne.location+'_selectedTerminal'] = 1;
}
// GateOne.Utils (generic utility functions)
View
1  setup.py
@@ -80,6 +80,7 @@ def walk_data_files(path, install_path=prefix):
(os.path.join(prefix, 'gateone'), [
os.path.join(setup_dir, 'gateone', 'auth.py'),
os.path.join(setup_dir, 'gateone', 'gateone.py'),
+ os.path.join(setup_dir, 'gateone', 'gopam.py'),
os.path.join(setup_dir, 'gateone', 'logviewer.py'),
os.path.join(setup_dir, 'gateone', 'sso.py'),
os.path.join(setup_dir, 'gateone', 'terminal.py'),
Please sign in to comment.
Something went wrong with that request. Please try again.