You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

411 lines
12 KiB

7 years ago
  1. # Copyright (C) 2003-2007 John Rochester <john@jrochester.org>
  2. #
  3. # This file is part of paramiko.
  4. #
  5. # Paramiko is free software; you can redistribute it and/or modify it under the
  6. # terms of the GNU Lesser General Public License as published by the Free
  7. # Software Foundation; either version 2.1 of the License, or (at your option)
  8. # any later version.
  9. #
  10. # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
  11. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  13. # details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
  17. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
  18. """
  19. SSH Agent interface
  20. """
  21. import os
  22. import socket
  23. import struct
  24. import sys
  25. import threading
  26. import time
  27. import tempfile
  28. import stat
  29. from select import select
  30. from paramiko.common import asbytes, io_sleep
  31. from paramiko.py3compat import byte_chr
  32. from paramiko.ssh_exception import SSHException, AuthenticationException
  33. from paramiko.message import Message
  34. from paramiko.pkey import PKey
  35. from paramiko.util import retry_on_signal
  36. cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11)
  37. SSH2_AGENT_IDENTITIES_ANSWER = 12
  38. cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13)
  39. SSH2_AGENT_SIGN_RESPONSE = 14
  40. class AgentSSH(object):
  41. def __init__(self):
  42. self._conn = None
  43. self._keys = ()
  44. def get_keys(self):
  45. """
  46. Return the list of keys available through the SSH agent, if any. If
  47. no SSH agent was running (or it couldn't be contacted), an empty list
  48. will be returned.
  49. :return:
  50. a tuple of `.AgentKey` objects representing keys available on the
  51. SSH agent
  52. """
  53. return self._keys
  54. def _connect(self, conn):
  55. self._conn = conn
  56. ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES)
  57. if ptype != SSH2_AGENT_IDENTITIES_ANSWER:
  58. raise SSHException('could not get keys from ssh-agent')
  59. keys = []
  60. for i in range(result.get_int()):
  61. keys.append(AgentKey(self, result.get_binary()))
  62. result.get_string()
  63. self._keys = tuple(keys)
  64. def _close(self):
  65. if self._conn is not None:
  66. self._conn.close()
  67. self._conn = None
  68. self._keys = ()
  69. def _send_message(self, msg):
  70. msg = asbytes(msg)
  71. self._conn.send(struct.pack('>I', len(msg)) + msg)
  72. l = self._read_all(4)
  73. msg = Message(self._read_all(struct.unpack('>I', l)[0]))
  74. return ord(msg.get_byte()), msg
  75. def _read_all(self, wanted):
  76. result = self._conn.recv(wanted)
  77. while len(result) < wanted:
  78. if len(result) == 0:
  79. raise SSHException('lost ssh-agent')
  80. extra = self._conn.recv(wanted - len(result))
  81. if len(extra) == 0:
  82. raise SSHException('lost ssh-agent')
  83. result += extra
  84. return result
  85. class AgentProxyThread(threading.Thread):
  86. """
  87. Class in charge of communication between two channels.
  88. """
  89. def __init__(self, agent):
  90. threading.Thread.__init__(self, target=self.run)
  91. self._agent = agent
  92. self._exit = False
  93. def run(self):
  94. try:
  95. (r, addr) = self.get_connection()
  96. # Found that r should be either
  97. # a socket from the socket library or None
  98. self.__inr = r
  99. # The address should be an IP address as a string? or None
  100. self.__addr = addr
  101. self._agent.connect()
  102. if (
  103. not isinstance(self._agent, int) and
  104. (
  105. self._agent._conn is None or
  106. not hasattr(self._agent._conn, 'fileno')
  107. )
  108. ):
  109. raise AuthenticationException("Unable to connect to SSH agent")
  110. self._communicate()
  111. except:
  112. # XXX Not sure what to do here ... raise or pass ?
  113. raise
  114. def _communicate(self):
  115. import fcntl
  116. oldflags = fcntl.fcntl(self.__inr, fcntl.F_GETFL)
  117. fcntl.fcntl(self.__inr, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
  118. while not self._exit:
  119. events = select([self._agent._conn, self.__inr], [], [], 0.5)
  120. for fd in events[0]:
  121. if self._agent._conn == fd:
  122. data = self._agent._conn.recv(512)
  123. if len(data) != 0:
  124. self.__inr.send(data)
  125. else:
  126. self._close()
  127. break
  128. elif self.__inr == fd:
  129. data = self.__inr.recv(512)
  130. if len(data) != 0:
  131. self._agent._conn.send(data)
  132. else:
  133. self._close()
  134. break
  135. time.sleep(io_sleep)
  136. def _close(self):
  137. self._exit = True
  138. self.__inr.close()
  139. self._agent._conn.close()
  140. class AgentLocalProxy(AgentProxyThread):
  141. """
  142. Class to be used when wanting to ask a local SSH Agent being
  143. asked from a remote fake agent (so use a unix socket for ex.)
  144. """
  145. def __init__(self, agent):
  146. AgentProxyThread.__init__(self, agent)
  147. def get_connection(self):
  148. """
  149. Return a pair of socket object and string address.
  150. May block!
  151. """
  152. conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  153. try:
  154. conn.bind(self._agent._get_filename())
  155. conn.listen(1)
  156. (r, addr) = conn.accept()
  157. return r, addr
  158. except:
  159. raise
  160. class AgentRemoteProxy(AgentProxyThread):
  161. """
  162. Class to be used when wanting to ask a remote SSH Agent
  163. """
  164. def __init__(self, agent, chan):
  165. AgentProxyThread.__init__(self, agent)
  166. self.__chan = chan
  167. def get_connection(self):
  168. return self.__chan, None
  169. class AgentClientProxy(object):
  170. """
  171. Class proxying request as a client:
  172. #. client ask for a request_forward_agent()
  173. #. server creates a proxy and a fake SSH Agent
  174. #. server ask for establishing a connection when needed,
  175. calling the forward_agent_handler at client side.
  176. #. the forward_agent_handler launch a thread for connecting
  177. the remote fake agent and the local agent
  178. #. Communication occurs ...
  179. """
  180. def __init__(self, chanRemote):
  181. self._conn = None
  182. self.__chanR = chanRemote
  183. self.thread = AgentRemoteProxy(self, chanRemote)
  184. self.thread.start()
  185. def __del__(self):
  186. self.close()
  187. def connect(self):
  188. """
  189. Method automatically called by ``AgentProxyThread.run``.
  190. """
  191. if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'):
  192. conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  193. try:
  194. retry_on_signal(
  195. lambda: conn.connect(os.environ['SSH_AUTH_SOCK']))
  196. except:
  197. # probably a dangling env var: the ssh agent is gone
  198. return
  199. elif sys.platform == 'win32':
  200. import paramiko.win_pageant as win_pageant
  201. if win_pageant.can_talk_to_agent():
  202. conn = win_pageant.PageantConnection()
  203. else:
  204. return
  205. else:
  206. # no agent support
  207. return
  208. self._conn = conn
  209. def close(self):
  210. """
  211. Close the current connection and terminate the agent
  212. Should be called manually
  213. """
  214. if hasattr(self, "thread"):
  215. self.thread._exit = True
  216. self.thread.join(1000)
  217. if self._conn is not None:
  218. self._conn.close()
  219. class AgentServerProxy(AgentSSH):
  220. """
  221. :param .Transport t: Transport used for SSH Agent communication forwarding
  222. :raises: `.SSHException` -- mostly if we lost the agent
  223. """
  224. def __init__(self, t):
  225. AgentSSH.__init__(self)
  226. self.__t = t
  227. self._dir = tempfile.mkdtemp('sshproxy')
  228. os.chmod(self._dir, stat.S_IRWXU)
  229. self._file = self._dir + '/sshproxy.ssh'
  230. self.thread = AgentLocalProxy(self)
  231. self.thread.start()
  232. def __del__(self):
  233. self.close()
  234. def connect(self):
  235. conn_sock = self.__t.open_forward_agent_channel()
  236. if conn_sock is None:
  237. raise SSHException('lost ssh-agent')
  238. conn_sock.set_name('auth-agent')
  239. self._connect(conn_sock)
  240. def close(self):
  241. """
  242. Terminate the agent, clean the files, close connections
  243. Should be called manually
  244. """
  245. os.remove(self._file)
  246. os.rmdir(self._dir)
  247. self.thread._exit = True
  248. self.thread.join(1000)
  249. self._close()
  250. def get_env(self):
  251. """
  252. Helper for the environnement under unix
  253. :return:
  254. a dict containing the ``SSH_AUTH_SOCK`` environnement variables
  255. """
  256. return {'SSH_AUTH_SOCK': self._get_filename()}
  257. def _get_filename(self):
  258. return self._file
  259. class AgentRequestHandler(object):
  260. """
  261. Primary/default implementation of SSH agent forwarding functionality.
  262. Simply instantiate this class, handing it a live command-executing session
  263. object, and it will handle forwarding any local SSH agent processes it
  264. finds.
  265. For example::
  266. # Connect
  267. client = SSHClient()
  268. client.connect(host, port, username)
  269. # Obtain session
  270. session = client.get_transport().open_session()
  271. # Forward local agent
  272. AgentRequestHandler(session)
  273. # Commands executed after this point will see the forwarded agent on
  274. # the remote end.
  275. session.exec_command("git clone https://my.git.repository/")
  276. """
  277. def __init__(self, chanClient):
  278. self._conn = None
  279. self.__chanC = chanClient
  280. chanClient.request_forward_agent(self._forward_agent_handler)
  281. self.__clientProxys = []
  282. def _forward_agent_handler(self, chanRemote):
  283. self.__clientProxys.append(AgentClientProxy(chanRemote))
  284. def __del__(self):
  285. self.close()
  286. def close(self):
  287. for p in self.__clientProxys:
  288. p.close()
  289. class Agent(AgentSSH):
  290. """
  291. Client interface for using private keys from an SSH agent running on the
  292. local machine. If an SSH agent is running, this class can be used to
  293. connect to it and retrieve `.PKey` objects which can be used when
  294. attempting to authenticate to remote SSH servers.
  295. Upon initialization, a session with the local machine's SSH agent is
  296. opened, if one is running. If no agent is running, initialization will
  297. succeed, but `get_keys` will return an empty tuple.
  298. :raises: `.SSHException` --
  299. if an SSH agent is found, but speaks an incompatible protocol
  300. """
  301. def __init__(self):
  302. AgentSSH.__init__(self)
  303. if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'):
  304. conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  305. try:
  306. conn.connect(os.environ['SSH_AUTH_SOCK'])
  307. except:
  308. # probably a dangling env var: the ssh agent is gone
  309. return
  310. elif sys.platform == 'win32':
  311. from . import win_pageant
  312. if win_pageant.can_talk_to_agent():
  313. conn = win_pageant.PageantConnection()
  314. else:
  315. return
  316. else:
  317. # no agent support
  318. return
  319. self._connect(conn)
  320. def close(self):
  321. """
  322. Close the SSH agent connection.
  323. """
  324. self._close()
  325. class AgentKey(PKey):
  326. """
  327. Private key held in a local SSH agent. This type of key can be used for
  328. authenticating to a remote server (signing). Most other key operations
  329. work as expected.
  330. """
  331. def __init__(self, agent, blob):
  332. self.agent = agent
  333. self.blob = blob
  334. self.public_blob = None
  335. self.name = Message(blob).get_text()
  336. def asbytes(self):
  337. return self.blob
  338. def __str__(self):
  339. return self.asbytes()
  340. def get_name(self):
  341. return self.name
  342. def sign_ssh_data(self, data):
  343. msg = Message()
  344. msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)
  345. msg.add_string(self.blob)
  346. msg.add_string(data)
  347. msg.add_int(0)
  348. ptype, result = self.agent._send_message(msg)
  349. if ptype != SSH2_AGENT_SIGN_RESPONSE:
  350. raise SSHException('key cannot be used for signing')
  351. return result.get_binary()

Powered by TurnKey Linux.