summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--NEWS17
-rwxr-xr-xdemos/demo.py1
-rw-r--r--paramiko/_winapi.py269
-rw-r--r--paramiko/client.py7
-rw-r--r--paramiko/hostkeys.py7
-rw-r--r--paramiko/win_pageant.py81
-rw-r--r--setup.py2
7 files changed, 323 insertions, 61 deletions
diff --git a/NEWS b/NEWS
index efee92d9..ef85effe 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,23 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
Releases
========
+v1.11.0 (DD MM YYYY)
+--------------------
+
+* #98: On Windows, when interacting with the PuTTY PAgeant, Paramiko now
+ creates the shared memory map with explicit Security Attributes of the user,
+ which is the same technique employed by the canonical PuTTY library to avoid
+ permissions issues when Paramiko is running under a different UAC context
+ than the PuTTY Ageant process. Thanks to Jason R. Coombs for the patch.
+* #100: Remove use of PyWin32 in `win_pageant` module. Module was already
+ dependent on ctypes for constructing appropriate structures and had ctypes
+ implementations of all functionality. Thanks to Jason R. Coombs for the
+ patch.
+* #87: Ensure updates to `known_hosts` files account for any updates to said
+ files after Paramiko initially read them. (Includes related fix to guard
+ against duplicate entries during subsequent `known_hosts` loads.) Thanks to
+ `@sunweaver` for the contribution.
+
v1.10.2 (DD MM 2013)
--------------------
diff --git a/demos/demo.py b/demos/demo.py
index 05524d3c..c21a926e 100755
--- a/demos/demo.py
+++ b/demos/demo.py
@@ -26,7 +26,6 @@ import os
import select
import socket
import sys
-import threading
import time
import traceback
diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py
new file mode 100644
index 00000000..f141b005
--- /dev/null
+++ b/paramiko/_winapi.py
@@ -0,0 +1,269 @@
+"""
+Windows API functions implemented as ctypes functions and classes as found
+in jaraco.windows (2.10).
+
+If you encounter issues with this module, please consider reporting the issues
+in jaraco.windows and asking the author to port the fixes back here.
+"""
+
+import ctypes
+import ctypes.wintypes
+import __builtin__
+
+######################
+# jaraco.windows.error
+
+def format_system_message(errno):
+ """
+ Call FormatMessage with a system error number to retrieve
+ the descriptive error message.
+ """
+ # first some flags used by FormatMessageW
+ ALLOCATE_BUFFER = 0x100
+ ARGUMENT_ARRAY = 0x2000
+ FROM_HMODULE = 0x800
+ FROM_STRING = 0x400
+ FROM_SYSTEM = 0x1000
+ IGNORE_INSERTS = 0x200
+
+ # Let FormatMessageW allocate the buffer (we'll free it below)
+ # Also, let it know we want a system error message.
+ flags = ALLOCATE_BUFFER | FROM_SYSTEM
+ source = None
+ message_id = errno
+ language_id = 0
+ result_buffer = ctypes.wintypes.LPWSTR()
+ buffer_size = 0
+ arguments = None
+ bytes = ctypes.windll.kernel32.FormatMessageW(
+ flags,
+ source,
+ message_id,
+ language_id,
+ ctypes.byref(result_buffer),
+ buffer_size,
+ arguments,
+ )
+ # note the following will cause an infinite loop if GetLastError
+ # repeatedly returns an error that cannot be formatted, although
+ # this should not happen.
+ handle_nonzero_success(bytes)
+ message = result_buffer.value
+ ctypes.windll.kernel32.LocalFree(result_buffer)
+ return message
+
+
+class WindowsError(__builtin__.WindowsError):
+ "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx"
+
+ def __init__(self, value=None):
+ if value is None:
+ value = ctypes.windll.kernel32.GetLastError()
+ strerror = format_system_message(value)
+ super(WindowsError, self).__init__(value, strerror)
+
+ @property
+ def message(self):
+ return self.strerror
+
+ @property
+ def code(self):
+ return self.winerror
+
+ def __str__(self):
+ return self.message
+
+ def __repr__(self):
+ return '{self.__class__.__name__}({self.winerror})'.format(**vars())
+
+def handle_nonzero_success(result):
+ if result == 0:
+ raise WindowsError()
+
+
+#####################
+# jaraco.windows.mmap
+
+CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW
+CreateFileMapping.argtypes = [
+ ctypes.wintypes.HANDLE,
+ ctypes.c_void_p,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.LPWSTR,
+]
+CreateFileMapping.restype = ctypes.wintypes.HANDLE
+
+MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile
+MapViewOfFile.restype = ctypes.wintypes.HANDLE
+
+class MemoryMap(object):
+ """
+ A memory map object which can have security attributes overrideden.
+ """
+ def __init__(self, name, length, security_attributes=None):
+ self.name = name
+ self.length = length
+ self.security_attributes = security_attributes
+ self.pos = 0
+
+ def __enter__(self):
+ p_SA = (
+ ctypes.byref(self.security_attributes)
+ if self.security_attributes else None
+ )
+ INVALID_HANDLE_VALUE = -1
+ PAGE_READWRITE = 0x4
+ FILE_MAP_WRITE = 0x2
+ filemap = ctypes.windll.kernel32.CreateFileMappingW(
+ INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length,
+ unicode(self.name))
+ handle_nonzero_success(filemap)
+ if filemap == INVALID_HANDLE_VALUE:
+ raise Exception("Failed to create file mapping")
+ self.filemap = filemap
+ self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0)
+ return self
+
+ def seek(self, pos):
+ self.pos = pos
+
+ def write(self, msg):
+ ctypes.windll.msvcrt.memcpy(self.view + self.pos, msg, len(msg))
+ self.pos += len(msg)
+
+ def read(self, n):
+ """
+ Read n bytes from mapped view.
+ """
+ out = ctypes.create_string_buffer(n)
+ ctypes.windll.msvcrt.memcpy(out, self.view + self.pos, n)
+ self.pos += n
+ return out.raw
+
+ def __exit__(self, exc_type, exc_val, tb):
+ ctypes.windll.kernel32.UnmapViewOfFile(self.view)
+ ctypes.windll.kernel32.CloseHandle(self.filemap)
+
+#########################
+# jaraco.windows.security
+
+class TokenInformationClass:
+ TokenUser = 1
+
+class TOKEN_USER(ctypes.Structure):
+ num = 1
+ _fields_ = [
+ ('SID', ctypes.c_void_p),
+ ('ATTRIBUTES', ctypes.wintypes.DWORD),
+ ]
+
+
+class SECURITY_DESCRIPTOR(ctypes.Structure):
+ """
+ typedef struct _SECURITY_DESCRIPTOR
+ {
+ UCHAR Revision;
+ UCHAR Sbz1;
+ SECURITY_DESCRIPTOR_CONTROL Control;
+ PSID Owner;
+ PSID Group;
+ PACL Sacl;
+ PACL Dacl;
+ } SECURITY_DESCRIPTOR;
+ """
+ SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT
+ REVISION = 1
+
+ _fields_ = [
+ ('Revision', ctypes.c_ubyte),
+ ('Sbz1', ctypes.c_ubyte),
+ ('Control', SECURITY_DESCRIPTOR_CONTROL),
+ ('Owner', ctypes.c_void_p),
+ ('Group', ctypes.c_void_p),
+ ('Sacl', ctypes.c_void_p),
+ ('Dacl', ctypes.c_void_p),
+ ]
+
+class SECURITY_ATTRIBUTES(ctypes.Structure):
+ """
+ typedef struct _SECURITY_ATTRIBUTES {
+ DWORD nLength;
+ LPVOID lpSecurityDescriptor;
+ BOOL bInheritHandle;
+ } SECURITY_ATTRIBUTES;
+ """
+ _fields_ = [
+ ('nLength', ctypes.wintypes.DWORD),
+ ('lpSecurityDescriptor', ctypes.c_void_p),
+ ('bInheritHandle', ctypes.wintypes.BOOL),
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs)
+ self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
+
+ def _get_descriptor(self):
+ return self._descriptor
+ def _set_descriptor(self, descriptor):
+ self._descriptor = descriptor
+ self.lpSecurityDescriptor = ctypes.addressof(descriptor)
+ descriptor = property(_get_descriptor, _set_descriptor)
+
+def GetTokenInformation(token, information_class):
+ """
+ Given a token, get the token information for it.
+ """
+ data_size = ctypes.wintypes.DWORD()
+ ctypes.windll.advapi32.GetTokenInformation(token, information_class.num,
+ 0, 0, ctypes.byref(data_size))
+ data = ctypes.create_string_buffer(data_size.value)
+ handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token,
+ information_class.num,
+ ctypes.byref(data), ctypes.sizeof(data),
+ ctypes.byref(data_size)))
+ return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents
+
+class TokenAccess:
+ TOKEN_QUERY = 0x8
+
+def OpenProcessToken(proc_handle, access):
+ result = ctypes.wintypes.HANDLE()
+ proc_handle = ctypes.wintypes.HANDLE(proc_handle)
+ handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken(
+ proc_handle, access, ctypes.byref(result)))
+ return result
+
+def get_current_user():
+ """
+ Return a TOKEN_USER for the owner of this process.
+ """
+ process = OpenProcessToken(
+ ctypes.windll.kernel32.GetCurrentProcess(),
+ TokenAccess.TOKEN_QUERY,
+ )
+ return GetTokenInformation(process, TOKEN_USER)
+
+def get_security_attributes_for_user(user=None):
+ """
+ Return a SECURITY_ATTRIBUTES structure with the SID set to the
+ specified user (uses current user if none is specified).
+ """
+ if user is None:
+ user = get_current_user()
+
+ assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance"
+
+ SD = SECURITY_DESCRIPTOR()
+ SA = SECURITY_ATTRIBUTES()
+ # by attaching the actual security descriptor, it will be garbage-
+ # collected with the security attributes
+ SA.descriptor = SD
+ SA.bInheritHandle = 1
+
+ ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD),
+ SECURITY_DESCRIPTOR.REVISION)
+ ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD),
+ user.SID, 0)
+ return SA
diff --git a/paramiko/client.py b/paramiko/client.py
index 5b719581..493d5481 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -186,8 +186,13 @@ class SSHClient (object):
@raise IOError: if the file could not be written
"""
+
+ # update local host keys from file (in case other SSH clients
+ # have written to the known_hosts file meanwhile.
+ if self.known_hosts is not None:
+ self.load_host_keys(self.known_hosts)
+
f = open(filename, 'w')
- f.write('# SSH host keys collected by paramiko\n')
for hostname, keys in self._host_keys.iteritems():
for keytype, key in keys.iteritems():
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 93a621d3..f64e6e63 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -171,7 +171,12 @@ class HostKeys (UserDict.DictMixin):
continue
e = HostKeyEntry.from_line(line, lineno)
if e is not None:
- self._entries.append(e)
+ _hostnames = e.hostnames
+ for h in _hostnames:
+ if self.check(h, e.key):
+ e.hostnames.remove(h)
+ if len(e.hostnames):
+ self._entries.append(e)
f.close()
def save(self, filename):
diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py
index d77d58fe..b96a740f 100644
--- a/paramiko/win_pageant.py
+++ b/paramiko/win_pageant.py
@@ -21,28 +21,15 @@
Functions for communicating with Pageant, the basic windows ssh agent program.
"""
-import os
+from __future__ import with_statement
+
import struct
-import tempfile
-import mmap
+import threading
import array
import platform
import ctypes.wintypes
-# if you're on windows, you should have one of these, i guess?
-# ctypes is part of standard library since Python 2.5
-_has_win32all = False
-_has_ctypes = False
-try:
- # win32gui is preferred over win32ui to avoid MFC dependencies
- import win32gui
- _has_win32all = True
-except ImportError:
- try:
- import ctypes
- _has_ctypes = True
- except ImportError:
- pass
+from . import _winapi
_AGENT_COPYDATA_ID = 0x804e50ba
_AGENT_MAX_MSGLEN = 8192
@@ -52,16 +39,7 @@ win32con_WM_COPYDATA = 74
def _get_pageant_window_object():
- if _has_win32all:
- try:
- hwnd = win32gui.FindWindow('Pageant', 'Pageant')
- return hwnd
- except win32gui.error:
- pass
- elif _has_ctypes:
- # Return 0 if there is no Pageant window.
- return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant')
- return None
+ return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant')
def can_talk_to_agent():
@@ -71,9 +49,7 @@ def can_talk_to_agent():
This checks both if we have the required libraries (win32all or ctypes)
and if there is a Pageant currently running.
"""
- if (_has_win32all or _has_ctypes) and _get_pageant_window_object():
- return True
- return False
+ return bool(_get_pageant_window_object())
ULONG_PTR = ctypes.c_uint64 if platform.architecture()[0] == '64bit' else ctypes.c_uint32
class COPYDATASTRUCT(ctypes.Structure):
@@ -88,48 +64,39 @@ class COPYDATASTRUCT(ctypes.Structure):
]
def _query_pageant(msg):
+ """
+ Communication with the Pageant process is done through a shared
+ memory-mapped file.
+ """
hwnd = _get_pageant_window_object()
if not hwnd:
# Raise a failure to connect exception, pageant isn't running anymore!
return None
- # Write our pageant request string into the file (pageant will read this to determine what to do)
- filename = tempfile.mktemp('.pag')
- map_filename = os.path.basename(filename)
-
- f = open(filename, 'w+b')
- f.write(msg )
- # Ensure the rest of the file is empty, otherwise pageant will read this
- f.write('\0' * (_AGENT_MAX_MSGLEN - len(msg)))
- # Create the shared file map that pageant will use to read from
- pymap = mmap.mmap(f.fileno(), _AGENT_MAX_MSGLEN, tagname=map_filename, access=mmap.ACCESS_WRITE)
- try:
+ # create a name for the mmap
+ map_name = 'PageantRequest%08x' % threading.current_thread().ident
+
+ pymap = _winapi.MemoryMap(map_name, _AGENT_MAX_MSGLEN,
+ _winapi.get_security_attributes_for_user(),
+ )
+ with pymap:
+ pymap.write(msg)
# Create an array buffer containing the mapped filename
- char_buffer = array.array("c", map_filename + '\0')
+ char_buffer = array.array("c", map_name + '\0')
char_buffer_address, char_buffer_size = char_buffer.buffer_info()
# Create a string to use for the SendMessage function call
- cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, char_buffer_address)
+ cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size,
+ char_buffer_address)
- if _has_win32all:
- # win32gui.SendMessage should also allow the same pattern as
- # ctypes, but let's keep it like this for now...
- response = win32gui.SendMessage(hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.addressof(cds))
- elif _has_ctypes:
- response = ctypes.windll.user32.SendMessageA(hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds))
- else:
- response = 0
+ response = ctypes.windll.user32.SendMessageA(hwnd,
+ win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds))
if response > 0:
+ pymap.seek(0)
datalen = pymap.read(4)
retlen = struct.unpack('>I', datalen)[0]
return datalen + pymap.read(retlen)
return None
- finally:
- pymap.close()
- f.close()
- # Remove the file, it was temporary only
- os.unlink(filename)
-
class PageantConnection (object):
"""
diff --git a/setup.py b/setup.py
index d6caccf7..02a71e58 100644
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,7 @@ if sys.platform == 'darwin':
setup(name = "paramiko",
- version = "1.10.1",
+ version = "1.11.0",
description = "SSH2 protocol library",
author = "Jeff Forcier",
author_email = "jeff@bitprophet.org",