1 # Copyright 2009 Shikhar Bhushan
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
18 from binascii import hexlify
19 from cStringIO import StringIO
20 from select import select
24 from errors import AuthenticationError, SessionCloseError, SSHError, SSHUnknownHostError
25 from session import Session
28 logger = logging.getLogger("ncclient.transport.ssh")
34 def default_unknown_host_cb(host, fingerprint):
35 """An unknown host callback returns `True` if it finds the key acceptable, and `False` if not.
37 This default callback always returns `False`, which would lead to :meth:`connect` raising a :exc:`SSHUnknownHost` exception.
39 Supply another valid callback if you need to verify the host key programatically.
41 *host* is the hostname that needs to be verified
43 *fingerprint* is a hex string representing the host key fingerprint, colon-delimited e.g. `"4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"`
49 for idx in range(2, len(fp), 2):
50 finga += ":" + fp[idx:idx+2]
53 class SSHSession(Session):
55 "Implements a :rfc:`4742` NETCONF session over SSH."
57 def __init__(self, capabilities):
58 Session.__init__(self, capabilities)
59 self._host_keys = paramiko.HostKeys()
60 self._transport = None
61 self._connected = False
63 self._buffer = StringIO() # for incoming data
64 # parsing-related, see _parse()
65 self._parsing_state = 0
69 "Messages ae delimited by MSG_DELIM. The buffer could have grown by a maximum of BUF_SIZE bytes everytime this method is called. Retains state across method calls and if a byte has been read it will not be considered again."
72 expect = self._parsing_state
74 buf.seek(self._parsing_pos)
77 if not x: # done reading
79 elif x == delim[expect]: # what we expected
80 expect += 1 # expect the next delim char
84 # loop till last delim char expected, break if other char encountered
85 for i in range(expect, n):
87 if not x: # done reading
89 if x == delim[expect]: # what we expected
90 expect += 1 # expect the next delim char
94 else: # if we didn't break out of the loop, full delim was parsed
95 msg_till = buf.tell() - n
97 logger.debug('parsed new message')
98 self._dispatch_message(buf.read(msg_till).strip())
99 buf.seek(n+1, os.SEEK_CUR)
106 self._parsing_state = expect
107 self._parsing_pos = self._buffer.tell()
109 def load_known_hosts(self, filename=None):
110 """Load host keys from an openssh :file:`known_hosts`-style file. Can be called multiple times.
112 If *filename* is not specified, looks in the default locations i.e. :file:`~/.ssh/known_hosts` and :file:`~/ssh/known_hosts` for Windows.
115 filename = os.path.expanduser('~/.ssh/known_hosts')
117 self._host_keys.load(filename)
120 filename = os.path.expanduser('~/ssh/known_hosts')
122 self._host_keys.load(filename)
126 self._host_keys.load(filename)
129 if self._transport.is_active():
130 self._transport.close()
131 self._connected = False
133 # REMEMBER to update transport.rst if sig. changes, since it is hardcoded there
134 def connect(self, host, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb,
135 username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True):
136 """Connect via SSH and initialize the NETCONF session. First attempts the publickey authentication method and then password authentication.
138 To disable attempting publickey authentication altogether, call with *allow_agent* and *look_for_keys* as `False`.
140 *host* is the hostname or IP address to connect to
142 *port* is by default 830, but some devices use the default SSH port of 22 so this may need to be specified
144 *timeout* is an optional timeout for socket connect
146 *unknown_host_cb* is called when the server host key is not recognized. It takes two arguments, the hostname and the fingerprint (see the signature of :func:`default_unknown_host_cb`)
148 *username* is the username to use for SSH authentication
150 *password* is the password used if using password authentication, or the passphrase to use for unlocking keys that require it
152 *key_filename* is a filename where a the private key to be used can be found
154 *allow_agent* enables querying SSH agent (if found) for keys
156 *look_for_keys* enables looking in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`)
159 username = getpass.getuser()
162 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
163 af, socktype, proto, canonname, sa = res
165 sock = socket.socket(af, socktype, proto)
166 sock.settimeout(timeout)
176 raise SSHError("Could not open socket to %s:%s" % (host, port))
178 t = self._transport = paramiko.Transport(sock)
179 t.set_log_channel(logger.name)
183 except paramiko.SSHException:
184 raise SSHError('Negotiation failed')
186 # host key verification
187 server_key = t.get_remote_server_key()
188 known_host = self._host_keys.check(host, server_key)
190 fingerprint = _colonify(hexlify(server_key.get_fingerprint()))
192 if not known_host and not unknown_host_cb(host, fingerprint):
193 raise SSHUnknownHostError(host, fingerprint)
195 if key_filename is None:
197 elif isinstance(key_filename, basestring):
198 key_filenames = [ key_filename ]
200 key_filenames = key_filename
202 self._auth(username, password, key_filenames, allow_agent, look_for_keys)
204 self._connected = True # there was no error authenticating
206 c = self._channel = self._transport.open_session()
207 c.set_name("netconf")
208 c.invoke_subsystem("netconf")
212 # on the lines of paramiko.SSHClient._auth()
213 def _auth(self, username, password, key_filenames, allow_agent,
215 saved_exception = None
217 for key_filename in key_filenames:
218 for cls in (paramiko.RSAKey, paramiko.DSSKey):
220 key = cls.from_private_key_file(key_filename, password)
221 logger.debug("Trying key %s from %s" %
222 (hexlify(key.get_fingerprint()), key_filename))
223 self._transport.auth_publickey(username, key)
225 except Exception as e:
230 for key in paramiko.Agent().get_keys():
232 logger.debug("Trying SSH agent key %s" %
233 hexlify(key.get_fingerprint()))
234 self._transport.auth_publickey(username, key)
236 except Exception as e:
242 rsa_key = os.path.expanduser("~/.ssh/id_rsa")
243 dsa_key = os.path.expanduser("~/.ssh/id_dsa")
244 if os.path.isfile(rsa_key):
245 keyfiles.append((paramiko.RSAKey, rsa_key))
246 if os.path.isfile(dsa_key):
247 keyfiles.append((paramiko.DSSKey, dsa_key))
248 # look in ~/ssh/ for windows users:
249 rsa_key = os.path.expanduser("~/ssh/id_rsa")
250 dsa_key = os.path.expanduser("~/ssh/id_dsa")
251 if os.path.isfile(rsa_key):
252 keyfiles.append((paramiko.RSAKey, rsa_key))
253 if os.path.isfile(dsa_key):
254 keyfiles.append((paramiko.DSSKey, dsa_key))
256 for cls, filename in keyfiles:
258 key = cls.from_private_key_file(filename, password)
259 logger.debug("Trying discovered key %s in %s" %
260 (hexlify(key.get_fingerprint()), filename))
261 self._transport.auth_publickey(username, key)
263 except Exception as e:
267 if password is not None:
269 self._transport.auth_password(username, password)
271 except Exception as e:
275 if saved_exception is not None:
276 # need pep-3134 to do this right
277 raise AuthenticationError(repr(saved_exception))
279 raise AuthenticationError("No authentication methods available")
287 # select on a paramiko ssh channel object does not ever return it in the writable list, so channels don't exactly emulate the socket api
288 r, w, e = select([chan], [], [], TICK)
289 # will wakeup evey TICK seconds to check if something to send, more if something to read (due to select returning chan in readable list)
291 data = chan.recv(BUF_SIZE)
293 self._buffer.write(data)
296 raise SessionCloseError(self._buffer.getvalue())
297 if not q.empty() and chan.send_ready():
298 logger.debug("Sending message")
299 data = q.get() + MSG_DELIM
303 raise SessionCloseError(self._buffer.getvalue(), data)
305 except Exception as e:
306 logger.debug("Broke out of main loop, error=%r", e)
308 self._dispatch_error(e)
312 "Underlying `paramiko.Transport <http://www.lag.net/paramiko/docs/paramiko.Transport-class.html>`_ object. This makes it possible to call methods like :meth:`~paramiko.Transport.set_keepalive` on it."
313 return self._transport