Open Media Library Platform
This commit is contained in:
commit
411ad5b16f
5849 changed files with 1778641 additions and 0 deletions
|
|
@ -0,0 +1,10 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""
|
||||
An SSHv2 implementation for Twisted. Part of the Twisted.Conch package.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_address -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Address object for SSH network connections.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
|
||||
@since: 12.1
|
||||
"""
|
||||
from zope.interface import implements
|
||||
from twisted.internet.interfaces import IAddress
|
||||
from twisted.python import util
|
||||
|
||||
|
||||
|
||||
class SSHTransportAddress(object, util.FancyEqMixin):
|
||||
"""
|
||||
Object representing an SSH Transport endpoint.
|
||||
|
||||
@ivar address: A instance of an object which implements I{IAddress} to
|
||||
which this transport address is connected.
|
||||
"""
|
||||
|
||||
implements(IAddress)
|
||||
|
||||
compareAttributes = ('address',)
|
||||
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
|
||||
def __repr__(self):
|
||||
return 'SSHTransportAddress(%r)' % (self.address,)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(('SSH', self.address))
|
||||
|
||||
294
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/agent.py
Normal file
294
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/agent.py
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implements the SSH v2 key agent protocol. This protocol is documented in the
|
||||
SSH source code, in the file
|
||||
U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.ssh.common import NS, getNS, getMP
|
||||
from twisted.conch.error import ConchError, MissingKeyStoreError
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.internet import defer, protocol
|
||||
|
||||
|
||||
|
||||
class SSHAgentClient(protocol.Protocol):
|
||||
"""
|
||||
The client side of the SSH agent protocol. This is equivalent to
|
||||
ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer
|
||||
protocol, also in this package.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = ''
|
||||
self.deferreds = []
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while 1:
|
||||
if len(self.buf) <= 4:
|
||||
return
|
||||
packLen = struct.unpack('!L', self.buf[:4])[0]
|
||||
if len(self.buf) < 4 + packLen:
|
||||
return
|
||||
packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
|
||||
reqType = ord(packet[0])
|
||||
d = self.deferreds.pop(0)
|
||||
if reqType == AGENT_FAILURE:
|
||||
d.errback(ConchError('agent failure'))
|
||||
elif reqType == AGENT_SUCCESS:
|
||||
d.callback('')
|
||||
else:
|
||||
d.callback(packet)
|
||||
|
||||
|
||||
def sendRequest(self, reqType, data):
|
||||
pack = struct.pack('!LB',len(data) + 1, reqType) + data
|
||||
self.transport.write(pack)
|
||||
d = defer.Deferred()
|
||||
self.deferreds.append(d)
|
||||
return d
|
||||
|
||||
|
||||
def requestIdentities(self):
|
||||
"""
|
||||
@return: A L{Deferred} which will fire with a list of all keys found in
|
||||
the SSH agent. The list of keys is comprised of (public key blob,
|
||||
comment) tuples.
|
||||
"""
|
||||
d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, '')
|
||||
d.addCallback(self._cbRequestIdentities)
|
||||
return d
|
||||
|
||||
|
||||
def _cbRequestIdentities(self, data):
|
||||
"""
|
||||
Unpack a collection of identities into a list of tuples comprised of
|
||||
public key blobs and comments.
|
||||
"""
|
||||
if ord(data[0]) != AGENT_IDENTITIES_ANSWER:
|
||||
raise ConchError('unexpected response: %i' % ord(data[0]))
|
||||
numKeys = struct.unpack('!L', data[1:5])[0]
|
||||
keys = []
|
||||
data = data[5:]
|
||||
for i in range(numKeys):
|
||||
blob, data = getNS(data)
|
||||
comment, data = getNS(data)
|
||||
keys.append((blob, comment))
|
||||
return keys
|
||||
|
||||
|
||||
def addIdentity(self, blob, comment = ''):
|
||||
"""
|
||||
Add a private key blob to the agent's collection of keys.
|
||||
"""
|
||||
req = blob
|
||||
req += NS(comment)
|
||||
return self.sendRequest(AGENTC_ADD_IDENTITY, req)
|
||||
|
||||
|
||||
def signData(self, blob, data):
|
||||
"""
|
||||
Request that the agent sign the given C{data} with the private key
|
||||
which corresponds to the public key given by C{blob}. The private
|
||||
key should have been added to the agent already.
|
||||
|
||||
@type blob: C{str}
|
||||
@type data: C{str}
|
||||
@return: A L{Deferred} which fires with a signature for given data
|
||||
created with the given key.
|
||||
"""
|
||||
req = NS(blob)
|
||||
req += NS(data)
|
||||
req += '\000\000\000\000' # flags
|
||||
return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData)
|
||||
|
||||
|
||||
def _cbSignData(self, data):
|
||||
if ord(data[0]) != AGENT_SIGN_RESPONSE:
|
||||
raise ConchError('unexpected data: %i' % ord(data[0]))
|
||||
signature = getNS(data[1:])[0]
|
||||
return signature
|
||||
|
||||
|
||||
def removeIdentity(self, blob):
|
||||
"""
|
||||
Remove the private key corresponding to the public key in blob from the
|
||||
running agent.
|
||||
"""
|
||||
req = NS(blob)
|
||||
return self.sendRequest(AGENTC_REMOVE_IDENTITY, req)
|
||||
|
||||
|
||||
def removeAllIdentities(self):
|
||||
"""
|
||||
Remove all keys from the running agent.
|
||||
"""
|
||||
return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, '')
|
||||
|
||||
|
||||
|
||||
class SSHAgentServer(protocol.Protocol):
|
||||
"""
|
||||
The server side of the SSH agent protocol. This is equivalent to
|
||||
ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient
|
||||
protocol, also in this package.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = ''
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while 1:
|
||||
if len(self.buf) <= 4:
|
||||
return
|
||||
packLen = struct.unpack('!L', self.buf[:4])[0]
|
||||
if len(self.buf) < 4 + packLen:
|
||||
return
|
||||
packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
|
||||
reqType = ord(packet[0])
|
||||
reqName = messages.get(reqType, None)
|
||||
if not reqName:
|
||||
self.sendResponse(AGENT_FAILURE, '')
|
||||
else:
|
||||
f = getattr(self, 'agentc_%s' % reqName)
|
||||
if getattr(self.factory, 'keys', None) is None:
|
||||
self.sendResponse(AGENT_FAILURE, '')
|
||||
raise MissingKeyStoreError()
|
||||
f(packet[1:])
|
||||
|
||||
|
||||
def sendResponse(self, reqType, data):
|
||||
pack = struct.pack('!LB', len(data) + 1, reqType) + data
|
||||
self.transport.write(pack)
|
||||
|
||||
|
||||
def agentc_REQUEST_IDENTITIES(self, data):
|
||||
"""
|
||||
Return all of the identities that have been added to the server
|
||||
"""
|
||||
assert data == ''
|
||||
numKeys = len(self.factory.keys)
|
||||
resp = []
|
||||
|
||||
resp.append(struct.pack('!L', numKeys))
|
||||
for key, comment in self.factory.keys.itervalues():
|
||||
resp.append(NS(key.blob())) # yes, wrapped in an NS
|
||||
resp.append(NS(comment))
|
||||
self.sendResponse(AGENT_IDENTITIES_ANSWER, ''.join(resp))
|
||||
|
||||
|
||||
def agentc_SIGN_REQUEST(self, data):
|
||||
"""
|
||||
Data is a structure with a reference to an already added key object and
|
||||
some data that the clients wants signed with that key. If the key
|
||||
object wasn't loaded, return AGENT_FAILURE, else return the signature.
|
||||
"""
|
||||
blob, data = getNS(data)
|
||||
if blob not in self.factory.keys:
|
||||
return self.sendResponse(AGENT_FAILURE, '')
|
||||
signData, data = getNS(data)
|
||||
assert data == '\000\000\000\000'
|
||||
self.sendResponse(AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData)))
|
||||
|
||||
|
||||
def agentc_ADD_IDENTITY(self, data):
|
||||
"""
|
||||
Adds a private key to the agent's collection of identities. On
|
||||
subsequent interactions, the private key can be accessed using only the
|
||||
corresponding public key.
|
||||
"""
|
||||
|
||||
# need to pre-read the key data so we can get past it to the comment string
|
||||
keyType, rest = getNS(data)
|
||||
if keyType == 'ssh-rsa':
|
||||
nmp = 6
|
||||
elif keyType == 'ssh-dss':
|
||||
nmp = 5
|
||||
else:
|
||||
raise keys.BadKeyError('unknown blob type: %s' % keyType)
|
||||
|
||||
rest = getMP(rest, nmp)[-1] # ignore the key data for now, we just want the comment
|
||||
comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob
|
||||
|
||||
k = keys.Key.fromString(data, type='private_blob') # not wrapped in NS here
|
||||
self.factory.keys[k.blob()] = (k, comment)
|
||||
self.sendResponse(AGENT_SUCCESS, '')
|
||||
|
||||
|
||||
def agentc_REMOVE_IDENTITY(self, data):
|
||||
"""
|
||||
Remove a specific key from the agent's collection of identities.
|
||||
"""
|
||||
blob, _ = getNS(data)
|
||||
k = keys.Key.fromString(blob, type='blob')
|
||||
del self.factory.keys[k.blob()]
|
||||
self.sendResponse(AGENT_SUCCESS, '')
|
||||
|
||||
|
||||
def agentc_REMOVE_ALL_IDENTITIES(self, data):
|
||||
"""
|
||||
Remove all keys from the agent's collection of identities.
|
||||
"""
|
||||
assert data == ''
|
||||
self.factory.keys = {}
|
||||
self.sendResponse(AGENT_SUCCESS, '')
|
||||
|
||||
# v1 messages that we ignore because we don't keep v1 keys
|
||||
# open-ssh sends both v1 and v2 commands, so we have to
|
||||
# do no-ops for v1 commands or we'll get "bad request" errors
|
||||
|
||||
def agentc_REQUEST_RSA_IDENTITIES(self, data):
|
||||
"""
|
||||
v1 message for listing RSA1 keys; superseded by
|
||||
agentc_REQUEST_IDENTITIES, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack('!L', 0))
|
||||
|
||||
|
||||
def agentc_REMOVE_RSA_IDENTITY(self, data):
|
||||
"""
|
||||
v1 message for removing RSA1 keys; superseded by
|
||||
agentc_REMOVE_IDENTITY, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_SUCCESS, '')
|
||||
|
||||
|
||||
def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data):
|
||||
"""
|
||||
v1 message for removing all RSA1 keys; superseded by
|
||||
agentc_REMOVE_ALL_IDENTITIES, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_SUCCESS, '')
|
||||
|
||||
|
||||
AGENTC_REQUEST_RSA_IDENTITIES = 1
|
||||
AGENT_RSA_IDENTITIES_ANSWER = 2
|
||||
AGENT_FAILURE = 5
|
||||
AGENT_SUCCESS = 6
|
||||
|
||||
AGENTC_REMOVE_RSA_IDENTITY = 8
|
||||
AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
|
||||
|
||||
AGENTC_REQUEST_IDENTITIES = 11
|
||||
AGENT_IDENTITIES_ANSWER = 12
|
||||
AGENTC_SIGN_REQUEST = 13
|
||||
AGENT_SIGN_RESPONSE = 14
|
||||
AGENTC_ADD_IDENTITY = 17
|
||||
AGENTC_REMOVE_IDENTITY = 18
|
||||
AGENTC_REMOVE_ALL_IDENTITIES = 19
|
||||
|
||||
messages = {}
|
||||
for name, value in locals().copy().items():
|
||||
if name[:7] == 'AGENTC_':
|
||||
messages[value] = name[7:] # doesn't handle doubles
|
||||
|
||||
281
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/channel.py
Normal file
281
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/channel.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_channel -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
"""
|
||||
The parent class for all the SSH Channels. Currently implemented channels
|
||||
are session. direct-tcp, and forwarded-tcp.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.internet import interfaces
|
||||
from zope.interface import implements
|
||||
|
||||
|
||||
class SSHChannel(log.Logger):
|
||||
"""
|
||||
A class that represents a multiplexed channel over an SSH connection.
|
||||
The channel has a local window which is the maximum amount of data it will
|
||||
receive, and a remote which is the maximum amount of data the remote side
|
||||
will accept. There is also a maximum packet size for any individual data
|
||||
packet going each way.
|
||||
|
||||
@ivar name: the name of the channel.
|
||||
@type name: C{str}
|
||||
@ivar localWindowSize: the maximum size of the local window in bytes.
|
||||
@type localWindowSize: C{int}
|
||||
@ivar localWindowLeft: how many bytes are left in the local window.
|
||||
@type localWindowLeft: C{int}
|
||||
@ivar localMaxPacket: the maximum size of packet we will accept in bytes.
|
||||
@type localMaxPacket: C{int}
|
||||
@ivar remoteWindowLeft: how many bytes are left in the remote window.
|
||||
@type remoteWindowLeft: C{int}
|
||||
@ivar remoteMaxPacket: the maximum size of a packet the remote side will
|
||||
accept in bytes.
|
||||
@type remoteMaxPacket: C{int}
|
||||
@ivar conn: the connection this channel is multiplexed through.
|
||||
@type conn: L{SSHConnection}
|
||||
@ivar data: any data to send to the other size when the channel is
|
||||
requested.
|
||||
@type data: C{str}
|
||||
@ivar avatar: an avatar for the logged-in user (if a server channel)
|
||||
@ivar localClosed: True if we aren't accepting more data.
|
||||
@type localClosed: C{bool}
|
||||
@ivar remoteClosed: True if the other size isn't accepting more data.
|
||||
@type remoteClosed: C{bool}
|
||||
"""
|
||||
|
||||
implements(interfaces.ITransport)
|
||||
|
||||
name = None # only needed for client channels
|
||||
|
||||
def __init__(self, localWindow = 0, localMaxPacket = 0,
|
||||
remoteWindow = 0, remoteMaxPacket = 0,
|
||||
conn = None, data=None, avatar = None):
|
||||
self.localWindowSize = localWindow or 131072
|
||||
self.localWindowLeft = self.localWindowSize
|
||||
self.localMaxPacket = localMaxPacket or 32768
|
||||
self.remoteWindowLeft = remoteWindow
|
||||
self.remoteMaxPacket = remoteMaxPacket
|
||||
self.areWriting = 1
|
||||
self.conn = conn
|
||||
self.data = data
|
||||
self.avatar = avatar
|
||||
self.specificData = ''
|
||||
self.buf = ''
|
||||
self.extBuf = []
|
||||
self.closing = 0
|
||||
self.localClosed = 0
|
||||
self.remoteClosed = 0
|
||||
self.id = None # gets set later by SSHConnection
|
||||
|
||||
def __str__(self):
|
||||
return '<SSHChannel %s (lw %i rw %i)>' % (self.name,
|
||||
self.localWindowLeft, self.remoteWindowLeft)
|
||||
|
||||
def logPrefix(self):
|
||||
id = (self.id is not None and str(self.id)) or "unknown"
|
||||
return "SSHChannel %s (%s) on %s" % (self.name, id,
|
||||
self.conn.logPrefix())
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
Called when the channel is opened. specificData is any data that the
|
||||
other side sent us when opening the channel.
|
||||
|
||||
@type specificData: C{str}
|
||||
"""
|
||||
log.msg('channel open')
|
||||
|
||||
def openFailed(self, reason):
|
||||
"""
|
||||
Called when the the open failed for some reason.
|
||||
reason.desc is a string descrption, reason.code the the SSH error code.
|
||||
|
||||
@type reason: L{error.ConchError}
|
||||
"""
|
||||
log.msg('other side refused open\nreason: %s'% reason)
|
||||
|
||||
def addWindowBytes(self, bytes):
|
||||
"""
|
||||
Called when bytes are added to the remote window. By default it clears
|
||||
the data buffers.
|
||||
|
||||
@type bytes: C{int}
|
||||
"""
|
||||
self.remoteWindowLeft = self.remoteWindowLeft+bytes
|
||||
if not self.areWriting and not self.closing:
|
||||
self.areWriting = True
|
||||
self.startWriting()
|
||||
if self.buf:
|
||||
b = self.buf
|
||||
self.buf = ''
|
||||
self.write(b)
|
||||
if self.extBuf:
|
||||
b = self.extBuf
|
||||
self.extBuf = []
|
||||
for (type, data) in b:
|
||||
self.writeExtended(type, data)
|
||||
|
||||
def requestReceived(self, requestType, data):
|
||||
"""
|
||||
Called when a request is sent to this channel. By default it delegates
|
||||
to self.request_<requestType>.
|
||||
If this function returns true, the request succeeded, otherwise it
|
||||
failed.
|
||||
|
||||
@type requestType: C{str}
|
||||
@type data: C{str}
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
foo = requestType.replace('-', '_')
|
||||
f = getattr(self, 'request_%s'%foo, None)
|
||||
if f:
|
||||
return f(data)
|
||||
log.msg('unhandled request for %s'%requestType)
|
||||
return 0
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Called when we receive data.
|
||||
|
||||
@type data: C{str}
|
||||
"""
|
||||
log.msg('got data %s'%repr(data))
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
"""
|
||||
Called when we receive extended data (usually standard error).
|
||||
|
||||
@type dataType: C{int}
|
||||
@type data: C{str}
|
||||
"""
|
||||
log.msg('got extended data %s %s'%(dataType, repr(data)))
|
||||
|
||||
def eofReceived(self):
|
||||
"""
|
||||
Called when the other side will send no more data.
|
||||
"""
|
||||
log.msg('remote eof')
|
||||
|
||||
def closeReceived(self):
|
||||
"""
|
||||
Called when the other side has closed the channel.
|
||||
"""
|
||||
log.msg('remote close')
|
||||
self.loseConnection()
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
Called when the channel is closed. This means that both our side and
|
||||
the remote side have closed the channel.
|
||||
"""
|
||||
log.msg('closed')
|
||||
|
||||
# transport stuff
|
||||
def write(self, data):
|
||||
"""
|
||||
Write some data to the channel. If there is not enough remote window
|
||||
available, buffer until it is. Otherwise, split the data into
|
||||
packets of length remoteMaxPacket and send them.
|
||||
|
||||
@type data: C{str}
|
||||
"""
|
||||
if self.buf:
|
||||
self.buf += data
|
||||
return
|
||||
top = len(data)
|
||||
if top > self.remoteWindowLeft:
|
||||
data, self.buf = (data[:self.remoteWindowLeft],
|
||||
data[self.remoteWindowLeft:])
|
||||
self.areWriting = 0
|
||||
self.stopWriting()
|
||||
top = self.remoteWindowLeft
|
||||
rmp = self.remoteMaxPacket
|
||||
write = self.conn.sendData
|
||||
r = range(0, top, rmp)
|
||||
for offset in r:
|
||||
write(self, data[offset: offset+rmp])
|
||||
self.remoteWindowLeft -= top
|
||||
if self.closing and not self.buf:
|
||||
self.loseConnection() # try again
|
||||
|
||||
def writeExtended(self, dataType, data):
|
||||
"""
|
||||
Send extended data to this channel. If there is not enough remote
|
||||
window available, buffer until there is. Otherwise, split the data
|
||||
into packets of length remoteMaxPacket and send them.
|
||||
|
||||
@type dataType: C{int}
|
||||
@type data: C{str}
|
||||
"""
|
||||
if self.extBuf:
|
||||
if self.extBuf[-1][0] == dataType:
|
||||
self.extBuf[-1][1] += data
|
||||
else:
|
||||
self.extBuf.append([dataType, data])
|
||||
return
|
||||
if len(data) > self.remoteWindowLeft:
|
||||
data, self.extBuf = (data[:self.remoteWindowLeft],
|
||||
[[dataType, data[self.remoteWindowLeft:]]])
|
||||
self.areWriting = 0
|
||||
self.stopWriting()
|
||||
while len(data) > self.remoteMaxPacket:
|
||||
self.conn.sendExtendedData(self, dataType,
|
||||
data[:self.remoteMaxPacket])
|
||||
data = data[self.remoteMaxPacket:]
|
||||
self.remoteWindowLeft -= self.remoteMaxPacket
|
||||
if data:
|
||||
self.conn.sendExtendedData(self, dataType, data)
|
||||
self.remoteWindowLeft -= len(data)
|
||||
if self.closing:
|
||||
self.loseConnection() # try again
|
||||
|
||||
def writeSequence(self, data):
|
||||
"""
|
||||
Part of the Transport interface. Write a list of strings to the
|
||||
channel.
|
||||
|
||||
@type data: C{list} of C{str}
|
||||
"""
|
||||
self.write(''.join(data))
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Close the channel if there is no buferred data. Otherwise, note the
|
||||
request and return.
|
||||
"""
|
||||
self.closing = 1
|
||||
if not self.buf and not self.extBuf:
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
Return a tuple describing the other side of the connection.
|
||||
|
||||
@rtype: C{tuple}
|
||||
"""
|
||||
return('SSH', )+self.conn.transport.getPeer()
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
Return a tuple describing our side of the connection.
|
||||
|
||||
@rtype: C{tuple}
|
||||
"""
|
||||
return('SSH', )+self.conn.transport.getHost()
|
||||
|
||||
def stopWriting(self):
|
||||
"""
|
||||
Called when the remote buffer is full, as a hint to stop writing.
|
||||
This can be ignored, but it can be helpful.
|
||||
"""
|
||||
|
||||
def startWriting(self):
|
||||
"""
|
||||
Called when the remote buffer has more room, as a hint to continue
|
||||
writing.
|
||||
"""
|
||||
117
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/common.py
Normal file
117
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/common.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_ssh -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Common functions for the SSH classes.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import struct, warnings, __builtin__
|
||||
|
||||
try:
|
||||
from Crypto import Util
|
||||
except ImportError:
|
||||
warnings.warn("PyCrypto not installed, but continuing anyways!",
|
||||
RuntimeWarning)
|
||||
|
||||
from twisted.python import randbytes
|
||||
|
||||
|
||||
def NS(t):
|
||||
"""
|
||||
net string
|
||||
"""
|
||||
return struct.pack('!L',len(t)) + t
|
||||
|
||||
def getNS(s, count=1):
|
||||
"""
|
||||
get net string
|
||||
"""
|
||||
ns = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
l, = struct.unpack('!L',s[c:c+4])
|
||||
ns.append(s[c+4:4+l+c])
|
||||
c += 4 + l
|
||||
return tuple(ns) + (s[c:],)
|
||||
|
||||
def MP(number):
|
||||
if number==0: return '\000'*4
|
||||
assert number>0
|
||||
bn = Util.number.long_to_bytes(number)
|
||||
if ord(bn[0])&128:
|
||||
bn = '\000' + bn
|
||||
return struct.pack('>L',len(bn)) + bn
|
||||
|
||||
def getMP(data, count=1):
|
||||
"""
|
||||
Get multiple precision integer out of the string. A multiple precision
|
||||
integer is stored as a 4-byte length followed by length bytes of the
|
||||
integer. If count is specified, get count integers out of the string.
|
||||
The return value is a tuple of count integers followed by the rest of
|
||||
the data.
|
||||
"""
|
||||
mp = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
length, = struct.unpack('>L',data[c:c+4])
|
||||
mp.append(Util.number.bytes_to_long(data[c+4:c+4+length]))
|
||||
c += 4 + length
|
||||
return tuple(mp) + (data[c:],)
|
||||
|
||||
def _MPpow(x, y, z):
|
||||
"""return the MP version of (x**y)%z
|
||||
"""
|
||||
return MP(pow(x,y,z))
|
||||
|
||||
def ffs(c, s):
|
||||
"""
|
||||
first from second
|
||||
goes through the first list, looking for items in the second, returns the first one
|
||||
"""
|
||||
for i in c:
|
||||
if i in s: return i
|
||||
|
||||
getMP_py = getMP
|
||||
MP_py = MP
|
||||
_MPpow_py = _MPpow
|
||||
pyPow = pow
|
||||
|
||||
def _fastgetMP(data, count=1):
|
||||
mp = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
length = struct.unpack('!L', data[c:c+4])[0]
|
||||
mp.append(long(gmpy.mpz(data[c + 4:c + 4 + length][::-1] + '\x00', 256)))
|
||||
c += length + 4
|
||||
return tuple(mp) + (data[c:],)
|
||||
|
||||
def _fastMP(i):
|
||||
i2 = gmpy.mpz(i).binary()[::-1]
|
||||
return struct.pack('!L', len(i2)) + i2
|
||||
|
||||
def _fastMPpow(x, y, z=None):
|
||||
r = pyPow(gmpy.mpz(x),y,z).binary()[::-1]
|
||||
return struct.pack('!L', len(r)) + r
|
||||
|
||||
def install():
|
||||
global getMP, MP, _MPpow
|
||||
getMP = _fastgetMP
|
||||
MP = _fastMP
|
||||
_MPpow = _fastMPpow
|
||||
# XXX: We override builtin pow so that PyCrypto can benefit from gmpy too.
|
||||
def _fastpow(x, y, z=None, mpz=gmpy.mpz):
|
||||
if type(x) in (long, int):
|
||||
x = mpz(x)
|
||||
return pyPow(x, y, z)
|
||||
__builtin__.pow = _fastpow # evil evil
|
||||
|
||||
try:
|
||||
import gmpy
|
||||
install()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_connection -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of the ssh-connection service, which
|
||||
allows access to the shell and port-forwarding.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.ssh import service, common
|
||||
from twisted.conch import error
|
||||
from twisted.internet import defer
|
||||
from twisted.python import log
|
||||
|
||||
class SSHConnection(service.SSHService):
|
||||
"""
|
||||
An implementation of the 'ssh-connection' service. It is used to
|
||||
multiplex multiple channels over the single SSH connection.
|
||||
|
||||
@ivar localChannelID: the next number to use as a local channel ID.
|
||||
@type localChannelID: C{int}
|
||||
@ivar channels: a C{dict} mapping a local channel ID to C{SSHChannel}
|
||||
subclasses.
|
||||
@type channels: C{dict}
|
||||
@ivar localToRemoteChannel: a C{dict} mapping a local channel ID to a
|
||||
remote channel ID.
|
||||
@type localToRemoteChannel: C{dict}
|
||||
@ivar channelsToRemoteChannel: a C{dict} mapping a C{SSHChannel} subclass
|
||||
to remote channel ID.
|
||||
@type channelsToRemoteChannel: C{dict}
|
||||
@ivar deferreds: a C{dict} mapping a local channel ID to a C{list} of
|
||||
C{Deferreds} for outstanding channel requests. Also, the 'global'
|
||||
key stores the C{list} of pending global request C{Deferred}s.
|
||||
"""
|
||||
name = 'ssh-connection'
|
||||
|
||||
def __init__(self):
|
||||
self.localChannelID = 0 # this is the current # to use for channel ID
|
||||
self.localToRemoteChannel = {} # local channel ID -> remote channel ID
|
||||
self.channels = {} # local channel ID -> subclass of SSHChannel
|
||||
self.channelsToRemoteChannel = {} # subclass of SSHChannel ->
|
||||
# remote channel ID
|
||||
self.deferreds = {"global": []} # local channel -> list of deferreds
|
||||
# for pending requests or 'global' -> list of
|
||||
# deferreds for global requests
|
||||
self.transport = None # gets set later
|
||||
|
||||
|
||||
def serviceStarted(self):
|
||||
if hasattr(self.transport, 'avatar'):
|
||||
self.transport.avatar.conn = self
|
||||
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
Called when the connection is stopped.
|
||||
"""
|
||||
map(self.channelClosed, self.channels.values())
|
||||
self._cleanupGlobalDeferreds()
|
||||
|
||||
|
||||
def _cleanupGlobalDeferreds(self):
|
||||
"""
|
||||
All pending requests that have returned a deferred must be errbacked
|
||||
when this service is stopped, otherwise they might be left uncalled and
|
||||
uncallable.
|
||||
"""
|
||||
for d in self.deferreds["global"]:
|
||||
d.errback(error.ConchError("Connection stopped."))
|
||||
del self.deferreds["global"][:]
|
||||
|
||||
|
||||
# packet methods
|
||||
def ssh_GLOBAL_REQUEST(self, packet):
|
||||
"""
|
||||
The other side has made a global request. Payload::
|
||||
string request type
|
||||
bool want reply
|
||||
<request specific data>
|
||||
|
||||
This dispatches to self.gotGlobalRequest.
|
||||
"""
|
||||
requestType, rest = common.getNS(packet)
|
||||
wantReply, rest = ord(rest[0]), rest[1:]
|
||||
ret = self.gotGlobalRequest(requestType, rest)
|
||||
if wantReply:
|
||||
reply = MSG_REQUEST_FAILURE
|
||||
data = ''
|
||||
if ret:
|
||||
reply = MSG_REQUEST_SUCCESS
|
||||
if isinstance(ret, (tuple, list)):
|
||||
data = ret[1]
|
||||
self.transport.sendPacket(reply, data)
|
||||
|
||||
def ssh_REQUEST_SUCCESS(self, packet):
|
||||
"""
|
||||
Our global request succeeded. Get the appropriate Deferred and call
|
||||
it back with the packet we received.
|
||||
"""
|
||||
log.msg('RS')
|
||||
self.deferreds['global'].pop(0).callback(packet)
|
||||
|
||||
def ssh_REQUEST_FAILURE(self, packet):
|
||||
"""
|
||||
Our global request failed. Get the appropriate Deferred and errback
|
||||
it with the packet we received.
|
||||
"""
|
||||
log.msg('RF')
|
||||
self.deferreds['global'].pop(0).errback(
|
||||
error.ConchError('global request failed', packet))
|
||||
|
||||
def ssh_CHANNEL_OPEN(self, packet):
|
||||
"""
|
||||
The other side wants to get a channel. Payload::
|
||||
string channel name
|
||||
uint32 remote channel number
|
||||
uint32 remote window size
|
||||
uint32 remote maximum packet size
|
||||
<channel specific data>
|
||||
|
||||
We get a channel from self.getChannel(), give it a local channel number
|
||||
and notify the other side. Then notify the channel by calling its
|
||||
channelOpen method.
|
||||
"""
|
||||
channelType, rest = common.getNS(packet)
|
||||
senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[:12])
|
||||
packet = rest[12:]
|
||||
try:
|
||||
channel = self.getChannel(channelType, windowSize, maxPacket,
|
||||
packet)
|
||||
localChannel = self.localChannelID
|
||||
self.localChannelID += 1
|
||||
channel.id = localChannel
|
||||
self.channels[localChannel] = channel
|
||||
self.channelsToRemoteChannel[channel] = senderChannel
|
||||
self.localToRemoteChannel[localChannel] = senderChannel
|
||||
self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION,
|
||||
struct.pack('>4L', senderChannel, localChannel,
|
||||
channel.localWindowSize,
|
||||
channel.localMaxPacket)+channel.specificData)
|
||||
log.callWithLogger(channel, channel.channelOpen, packet)
|
||||
except Exception, e:
|
||||
log.err(e, 'channel open failed')
|
||||
if isinstance(e, error.ConchError):
|
||||
textualInfo, reason = e.args
|
||||
if isinstance(textualInfo, (int, long)):
|
||||
# See #3657 and #3071
|
||||
textualInfo, reason = reason, textualInfo
|
||||
else:
|
||||
reason = OPEN_CONNECT_FAILED
|
||||
textualInfo = "unknown failure"
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_OPEN_FAILURE,
|
||||
struct.pack('>2L', senderChannel, reason) +
|
||||
common.NS(textualInfo) + common.NS(''))
|
||||
|
||||
def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
|
||||
"""
|
||||
The other side accepted our MSG_CHANNEL_OPEN request. Payload::
|
||||
uint32 local channel number
|
||||
uint32 remote channel number
|
||||
uint32 remote window size
|
||||
uint32 remote maximum packet size
|
||||
<channel specific data>
|
||||
|
||||
Find the channel using the local channel number and notify its
|
||||
channelOpen method.
|
||||
"""
|
||||
(localChannel, remoteChannel, windowSize,
|
||||
maxPacket) = struct.unpack('>4L', packet[: 16])
|
||||
specificData = packet[16:]
|
||||
channel = self.channels[localChannel]
|
||||
channel.conn = self
|
||||
self.localToRemoteChannel[localChannel] = remoteChannel
|
||||
self.channelsToRemoteChannel[channel] = remoteChannel
|
||||
channel.remoteWindowLeft = windowSize
|
||||
channel.remoteMaxPacket = maxPacket
|
||||
log.callWithLogger(channel, channel.channelOpen, specificData)
|
||||
|
||||
def ssh_CHANNEL_OPEN_FAILURE(self, packet):
|
||||
"""
|
||||
The other side did not accept our MSG_CHANNEL_OPEN request. Payload::
|
||||
uint32 local channel number
|
||||
uint32 reason code
|
||||
string reason description
|
||||
|
||||
Find the channel using the local channel number and notify it by
|
||||
calling its openFailed() method.
|
||||
"""
|
||||
localChannel, reasonCode = struct.unpack('>2L', packet[:8])
|
||||
reasonDesc = common.getNS(packet[8:])[0]
|
||||
channel = self.channels[localChannel]
|
||||
del self.channels[localChannel]
|
||||
channel.conn = self
|
||||
reason = error.ConchError(reasonDesc, reasonCode)
|
||||
log.callWithLogger(channel, channel.openFailed, reason)
|
||||
|
||||
def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
|
||||
"""
|
||||
The other side is adding bytes to its window. Payload::
|
||||
uint32 local channel number
|
||||
uint32 bytes to add
|
||||
|
||||
Call the channel's addWindowBytes() method to add new bytes to the
|
||||
remote window.
|
||||
"""
|
||||
localChannel, bytesToAdd = struct.unpack('>2L', packet[:8])
|
||||
channel = self.channels[localChannel]
|
||||
log.callWithLogger(channel, channel.addWindowBytes, bytesToAdd)
|
||||
|
||||
def ssh_CHANNEL_DATA(self, packet):
|
||||
"""
|
||||
The other side is sending us data. Payload::
|
||||
uint32 local channel number
|
||||
string data
|
||||
|
||||
Check to make sure the other side hasn't sent too much data (more
|
||||
than what's in the window, or more than the maximum packet size). If
|
||||
they have, close the channel. Otherwise, decrease the available
|
||||
window and pass the data to the channel's dataReceived().
|
||||
"""
|
||||
localChannel, dataLength = struct.unpack('>2L', packet[:8])
|
||||
channel = self.channels[localChannel]
|
||||
# XXX should this move to dataReceived to put client in charge?
|
||||
if (dataLength > channel.localWindowLeft or
|
||||
dataLength > channel.localMaxPacket): # more data than we want
|
||||
log.callWithLogger(channel, log.msg, 'too much data')
|
||||
self.sendClose(channel)
|
||||
return
|
||||
#packet = packet[:channel.localWindowLeft+4]
|
||||
data = common.getNS(packet[4:])[0]
|
||||
channel.localWindowLeft -= dataLength
|
||||
if channel.localWindowLeft < channel.localWindowSize // 2:
|
||||
self.adjustWindow(channel, channel.localWindowSize - \
|
||||
channel.localWindowLeft)
|
||||
#log.msg('local window left: %s/%s' % (channel.localWindowLeft,
|
||||
# channel.localWindowSize))
|
||||
log.callWithLogger(channel, channel.dataReceived, data)
|
||||
|
||||
def ssh_CHANNEL_EXTENDED_DATA(self, packet):
|
||||
"""
|
||||
The other side is sending us exteneded data. Payload::
|
||||
uint32 local channel number
|
||||
uint32 type code
|
||||
string data
|
||||
|
||||
Check to make sure the other side hasn't sent too much data (more
|
||||
than what's in the window, or or than the maximum packet size). If
|
||||
they have, close the channel. Otherwise, decrease the available
|
||||
window and pass the data and type code to the channel's
|
||||
extReceived().
|
||||
"""
|
||||
localChannel, typeCode, dataLength = struct.unpack('>3L', packet[:12])
|
||||
channel = self.channels[localChannel]
|
||||
if (dataLength > channel.localWindowLeft or
|
||||
dataLength > channel.localMaxPacket):
|
||||
log.callWithLogger(channel, log.msg, 'too much extdata')
|
||||
self.sendClose(channel)
|
||||
return
|
||||
data = common.getNS(packet[8:])[0]
|
||||
channel.localWindowLeft -= dataLength
|
||||
if channel.localWindowLeft < channel.localWindowSize // 2:
|
||||
self.adjustWindow(channel, channel.localWindowSize -
|
||||
channel.localWindowLeft)
|
||||
log.callWithLogger(channel, channel.extReceived, typeCode, data)
|
||||
|
||||
def ssh_CHANNEL_EOF(self, packet):
|
||||
"""
|
||||
The other side is not sending any more data. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Notify the channel by calling its eofReceived() method.
|
||||
"""
|
||||
localChannel = struct.unpack('>L', packet[:4])[0]
|
||||
channel = self.channels[localChannel]
|
||||
log.callWithLogger(channel, channel.eofReceived)
|
||||
|
||||
def ssh_CHANNEL_CLOSE(self, packet):
|
||||
"""
|
||||
The other side is closing its end; it does not want to receive any
|
||||
more data. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Notify the channnel by calling its closeReceived() method. If
|
||||
the channel has also sent a close message, call self.channelClosed().
|
||||
"""
|
||||
localChannel = struct.unpack('>L', packet[:4])[0]
|
||||
channel = self.channels[localChannel]
|
||||
log.callWithLogger(channel, channel.closeReceived)
|
||||
channel.remoteClosed = True
|
||||
if channel.localClosed and channel.remoteClosed:
|
||||
self.channelClosed(channel)
|
||||
|
||||
def ssh_CHANNEL_REQUEST(self, packet):
|
||||
"""
|
||||
The other side is sending a request to a channel. Payload::
|
||||
uint32 local channel number
|
||||
string request name
|
||||
bool want reply
|
||||
<request specific data>
|
||||
|
||||
Pass the message to the channel's requestReceived method. If the
|
||||
other side wants a reply, add callbacks which will send the
|
||||
reply.
|
||||
"""
|
||||
localChannel = struct.unpack('>L', packet[:4])[0]
|
||||
requestType, rest = common.getNS(packet[4:])
|
||||
wantReply = ord(rest[0])
|
||||
channel = self.channels[localChannel]
|
||||
d = defer.maybeDeferred(log.callWithLogger, channel,
|
||||
channel.requestReceived, requestType, rest[1:])
|
||||
if wantReply:
|
||||
d.addCallback(self._cbChannelRequest, localChannel)
|
||||
d.addErrback(self._ebChannelRequest, localChannel)
|
||||
return d
|
||||
|
||||
def _cbChannelRequest(self, result, localChannel):
|
||||
"""
|
||||
Called back if the other side wanted a reply to a channel request. If
|
||||
the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise
|
||||
a C{error.ConchError}
|
||||
|
||||
@param result: the value returned from the channel's requestReceived()
|
||||
method. If it's False, the request failed.
|
||||
@type result: C{bool}
|
||||
@param localChannel: the local channel ID of the channel to which the
|
||||
request was made.
|
||||
@type localChannel: C{int}
|
||||
@raises ConchError: if the result is False.
|
||||
"""
|
||||
if not result:
|
||||
raise error.ConchError('failed request')
|
||||
self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L',
|
||||
self.localToRemoteChannel[localChannel]))
|
||||
|
||||
def _ebChannelRequest(self, result, localChannel):
|
||||
"""
|
||||
Called if the other wisde wanted a reply to the channel requeset and
|
||||
the channel request failed.
|
||||
|
||||
@param result: a Failure, but it's not used.
|
||||
@param localChannel: the local channel ID of the channel to which the
|
||||
request was made.
|
||||
@type localChannel: C{int}
|
||||
"""
|
||||
self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L',
|
||||
self.localToRemoteChannel[localChannel]))
|
||||
|
||||
def ssh_CHANNEL_SUCCESS(self, packet):
|
||||
"""
|
||||
Our channel request to the other other side succeeded. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Get the C{Deferred} out of self.deferreds and call it back.
|
||||
"""
|
||||
localChannel = struct.unpack('>L', packet[:4])[0]
|
||||
if self.deferreds.get(localChannel):
|
||||
d = self.deferreds[localChannel].pop(0)
|
||||
log.callWithLogger(self.channels[localChannel],
|
||||
d.callback, '')
|
||||
|
||||
def ssh_CHANNEL_FAILURE(self, packet):
|
||||
"""
|
||||
Our channel request to the other side failed. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Get the C{Deferred} out of self.deferreds and errback it with a
|
||||
C{error.ConchError}.
|
||||
"""
|
||||
localChannel = struct.unpack('>L', packet[:4])[0]
|
||||
if self.deferreds.get(localChannel):
|
||||
d = self.deferreds[localChannel].pop(0)
|
||||
log.callWithLogger(self.channels[localChannel],
|
||||
d.errback,
|
||||
error.ConchError('channel request failed'))
|
||||
|
||||
# methods for users of the connection to call
|
||||
|
||||
def sendGlobalRequest(self, request, data, wantReply=0):
|
||||
"""
|
||||
Send a global request for this connection. Current this is only used
|
||||
for remote->local TCP forwarding.
|
||||
|
||||
@type request: C{str}
|
||||
@type data: C{str}
|
||||
@type wantReply: C{bool}
|
||||
@rtype C{Deferred}/C{None}
|
||||
"""
|
||||
self.transport.sendPacket(MSG_GLOBAL_REQUEST,
|
||||
common.NS(request)
|
||||
+ (wantReply and '\xff' or '\x00')
|
||||
+ data)
|
||||
if wantReply:
|
||||
d = defer.Deferred()
|
||||
self.deferreds['global'].append(d)
|
||||
return d
|
||||
|
||||
def openChannel(self, channel, extra=''):
|
||||
"""
|
||||
Open a new channel on this connection.
|
||||
|
||||
@type channel: subclass of C{SSHChannel}
|
||||
@type extra: C{str}
|
||||
"""
|
||||
log.msg('opening channel %s with %s %s'%(self.localChannelID,
|
||||
channel.localWindowSize, channel.localMaxPacket))
|
||||
self.transport.sendPacket(MSG_CHANNEL_OPEN, common.NS(channel.name)
|
||||
+ struct.pack('>3L', self.localChannelID,
|
||||
channel.localWindowSize, channel.localMaxPacket)
|
||||
+ extra)
|
||||
channel.id = self.localChannelID
|
||||
self.channels[self.localChannelID] = channel
|
||||
self.localChannelID += 1
|
||||
|
||||
def sendRequest(self, channel, requestType, data, wantReply=0):
|
||||
"""
|
||||
Send a request to a channel.
|
||||
|
||||
@type channel: subclass of C{SSHChannel}
|
||||
@type requestType: C{str}
|
||||
@type data: C{str}
|
||||
@type wantReply: C{bool}
|
||||
@rtype C{Deferred}/C{None}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return
|
||||
log.msg('sending request %s' % requestType)
|
||||
self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L',
|
||||
self.channelsToRemoteChannel[channel])
|
||||
+ common.NS(requestType)+chr(wantReply)
|
||||
+ data)
|
||||
if wantReply:
|
||||
d = defer.Deferred()
|
||||
self.deferreds.setdefault(channel.id, []).append(d)
|
||||
return d
|
||||
|
||||
def adjustWindow(self, channel, bytesToAdd):
|
||||
"""
|
||||
Tell the other side that we will receive more data. This should not
|
||||
normally need to be called as it is managed automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type bytesToAdd: C{int}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L',
|
||||
self.channelsToRemoteChannel[channel],
|
||||
bytesToAdd))
|
||||
log.msg('adding %i to %i in channel %i' % (bytesToAdd,
|
||||
channel.localWindowLeft, channel.id))
|
||||
channel.localWindowLeft += bytesToAdd
|
||||
|
||||
def sendData(self, channel, data):
|
||||
"""
|
||||
Send data to a channel. This should not normally be used: instead use
|
||||
channel.write(data) as it manages the window automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type data: C{str}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L',
|
||||
self.channelsToRemoteChannel[channel]) +
|
||||
common.NS(data))
|
||||
|
||||
def sendExtendedData(self, channel, dataType, data):
|
||||
"""
|
||||
Send extended data to a channel. This should not normally be used:
|
||||
instead use channel.writeExtendedData(data, dataType) as it manages
|
||||
the window automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type dataType: C{int}
|
||||
@type data: C{str}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L',
|
||||
self.channelsToRemoteChannel[channel],dataType) \
|
||||
+ common.NS(data))
|
||||
|
||||
def sendEOF(self, channel):
|
||||
"""
|
||||
Send an EOF (End of File) for a channel.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
log.msg('sending eof')
|
||||
self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L',
|
||||
self.channelsToRemoteChannel[channel]))
|
||||
|
||||
def sendClose(self, channel):
|
||||
"""
|
||||
Close a channel.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
log.msg('sending close %i' % channel.id)
|
||||
self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L',
|
||||
self.channelsToRemoteChannel[channel]))
|
||||
channel.localClosed = True
|
||||
if channel.localClosed and channel.remoteClosed:
|
||||
self.channelClosed(channel)
|
||||
|
||||
# methods to override
|
||||
def getChannel(self, channelType, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side requested a channel of some sort.
|
||||
channelType is the type of channel being requested,
|
||||
windowSize is the initial size of the remote window,
|
||||
maxPacket is the largest packet we should send,
|
||||
data is any other packet data (often nothing).
|
||||
|
||||
We return a subclass of L{SSHChannel}.
|
||||
|
||||
By default, this dispatches to a method 'channel_channelType' with any
|
||||
non-alphanumerics in the channelType replace with _'s. If it cannot
|
||||
find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
|
||||
The method is called with arguments of windowSize, maxPacket, data.
|
||||
|
||||
@type channelType: C{str}
|
||||
@type windowSize: C{int}
|
||||
@type maxPacket: C{int}
|
||||
@type data: C{str}
|
||||
@rtype: subclass of L{SSHChannel}/C{tuple}
|
||||
"""
|
||||
log.msg('got channel %s request' % channelType)
|
||||
if hasattr(self.transport, "avatar"): # this is a server!
|
||||
chan = self.transport.avatar.lookupChannel(channelType,
|
||||
windowSize,
|
||||
maxPacket,
|
||||
data)
|
||||
else:
|
||||
channelType = channelType.translate(TRANSLATE_TABLE)
|
||||
f = getattr(self, 'channel_%s' % channelType, None)
|
||||
if f is not None:
|
||||
chan = f(windowSize, maxPacket, data)
|
||||
else:
|
||||
chan = None
|
||||
if chan is None:
|
||||
raise error.ConchError('unknown channel',
|
||||
OPEN_UNKNOWN_CHANNEL_TYPE)
|
||||
else:
|
||||
chan.conn = self
|
||||
return chan
|
||||
|
||||
def gotGlobalRequest(self, requestType, data):
|
||||
"""
|
||||
We got a global request. pretty much, this is just used by the client
|
||||
to request that we forward a port from the server to the client.
|
||||
Returns either:
|
||||
- 1: request accepted
|
||||
- 1, <data>: request accepted with request specific data
|
||||
- 0: request denied
|
||||
|
||||
By default, this dispatches to a method 'global_requestType' with
|
||||
-'s in requestType replaced with _'s. The found method is passed data.
|
||||
If this method cannot be found, this method returns 0. Otherwise, it
|
||||
returns the return value of that method.
|
||||
|
||||
@type requestType: C{str}
|
||||
@type data: C{str}
|
||||
@rtype: C{int}/C{tuple}
|
||||
"""
|
||||
log.msg('got global %s request' % requestType)
|
||||
if hasattr(self.transport, 'avatar'): # this is a server!
|
||||
return self.transport.avatar.gotGlobalRequest(requestType, data)
|
||||
|
||||
requestType = requestType.replace('-','_')
|
||||
f = getattr(self, 'global_%s' % requestType, None)
|
||||
if not f:
|
||||
return 0
|
||||
return f(data)
|
||||
|
||||
def channelClosed(self, channel):
|
||||
"""
|
||||
Called when a channel is closed.
|
||||
It clears the local state related to the channel, and calls
|
||||
channel.closed().
|
||||
MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
|
||||
If you don't, things will break mysteriously.
|
||||
|
||||
@type channel: L{SSHChannel}
|
||||
"""
|
||||
if channel in self.channelsToRemoteChannel: # actually open
|
||||
channel.localClosed = channel.remoteClosed = True
|
||||
del self.localToRemoteChannel[channel.id]
|
||||
del self.channels[channel.id]
|
||||
del self.channelsToRemoteChannel[channel]
|
||||
for d in self.deferreds.setdefault(channel.id, []):
|
||||
d.errback(error.ConchError("Channel closed."))
|
||||
del self.deferreds[channel.id][:]
|
||||
log.callWithLogger(channel, channel.closed)
|
||||
|
||||
MSG_GLOBAL_REQUEST = 80
|
||||
MSG_REQUEST_SUCCESS = 81
|
||||
MSG_REQUEST_FAILURE = 82
|
||||
MSG_CHANNEL_OPEN = 90
|
||||
MSG_CHANNEL_OPEN_CONFIRMATION = 91
|
||||
MSG_CHANNEL_OPEN_FAILURE = 92
|
||||
MSG_CHANNEL_WINDOW_ADJUST = 93
|
||||
MSG_CHANNEL_DATA = 94
|
||||
MSG_CHANNEL_EXTENDED_DATA = 95
|
||||
MSG_CHANNEL_EOF = 96
|
||||
MSG_CHANNEL_CLOSE = 97
|
||||
MSG_CHANNEL_REQUEST = 98
|
||||
MSG_CHANNEL_SUCCESS = 99
|
||||
MSG_CHANNEL_FAILURE = 100
|
||||
|
||||
OPEN_ADMINISTRATIVELY_PROHIBITED = 1
|
||||
OPEN_CONNECT_FAILED = 2
|
||||
OPEN_UNKNOWN_CHANNEL_TYPE = 3
|
||||
OPEN_RESOURCE_SHORTAGE = 4
|
||||
|
||||
EXTENDED_DATA_STDERR = 1
|
||||
|
||||
messages = {}
|
||||
for name, value in locals().copy().items():
|
||||
if name[:4] == 'MSG_':
|
||||
messages[value] = name # doesn't handle doubles
|
||||
|
||||
import string
|
||||
alphanums = string.letters + string.digits
|
||||
TRANSLATE_TABLE = ''.join([chr(i) in alphanums and chr(i) or '_'
|
||||
for i in range(256)])
|
||||
SSHConnection.protocolMessages = messages
|
||||
120
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/factory.py
Normal file
120
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/factory.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A Factory for SSH servers, along with an OpenSSHFactory to use the same
|
||||
data sources as OpenSSH.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
from twisted.internet import protocol
|
||||
from twisted.python import log
|
||||
|
||||
from twisted.conch import error
|
||||
import transport, userauth, connection
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class SSHFactory(protocol.Factory):
|
||||
"""
|
||||
A Factory for SSH servers.
|
||||
"""
|
||||
protocol = transport.SSHServerTransport
|
||||
|
||||
services = {
|
||||
'ssh-userauth':userauth.SSHUserAuthServer,
|
||||
'ssh-connection':connection.SSHConnection
|
||||
}
|
||||
def startFactory(self):
|
||||
"""
|
||||
Check for public and private keys.
|
||||
"""
|
||||
if not hasattr(self,'publicKeys'):
|
||||
self.publicKeys = self.getPublicKeys()
|
||||
if not hasattr(self,'privateKeys'):
|
||||
self.privateKeys = self.getPrivateKeys()
|
||||
if not self.publicKeys or not self.privateKeys:
|
||||
raise error.ConchError('no host keys, failing')
|
||||
if not hasattr(self,'primes'):
|
||||
self.primes = self.getPrimes()
|
||||
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Create an instance of the server side of the SSH protocol.
|
||||
|
||||
@type addr: L{twisted.internet.interfaces.IAddress} provider
|
||||
@param addr: The address at which the server will listen.
|
||||
|
||||
@rtype: L{twisted.conch.ssh.SSHServerTransport}
|
||||
@return: The built transport.
|
||||
"""
|
||||
t = protocol.Factory.buildProtocol(self, addr)
|
||||
t.supportedPublicKeys = self.privateKeys.keys()
|
||||
if not self.primes:
|
||||
log.msg('disabling diffie-hellman-group-exchange because we '
|
||||
'cannot find moduli file')
|
||||
ske = t.supportedKeyExchanges[:]
|
||||
ske.remove('diffie-hellman-group-exchange-sha1')
|
||||
t.supportedKeyExchanges = ske
|
||||
return t
|
||||
|
||||
|
||||
def getPublicKeys(self):
|
||||
"""
|
||||
Called when the factory is started to get the public portions of the
|
||||
servers host keys. Returns a dictionary mapping SSH key types to
|
||||
public key strings.
|
||||
|
||||
@rtype: C{dict}
|
||||
"""
|
||||
raise NotImplementedError('getPublicKeys unimplemented')
|
||||
|
||||
|
||||
def getPrivateKeys(self):
|
||||
"""
|
||||
Called when the factory is started to get the private portions of the
|
||||
servers host keys. Returns a dictionary mapping SSH key types to
|
||||
C{Crypto.PublicKey.pubkey.pubkey} objects.
|
||||
|
||||
@rtype: C{dict}
|
||||
"""
|
||||
raise NotImplementedError('getPrivateKeys unimplemented')
|
||||
|
||||
|
||||
def getPrimes(self):
|
||||
"""
|
||||
Called when the factory is started to get Diffie-Hellman generators and
|
||||
primes to use. Returns a dictionary mapping number of bits to lists
|
||||
of tuple of (generator, prime).
|
||||
|
||||
@rtype: C{dict}
|
||||
"""
|
||||
|
||||
|
||||
def getDHPrime(self, bits):
|
||||
"""
|
||||
Return a tuple of (g, p) for a Diffe-Hellman process, with p being as
|
||||
close to bits bits as possible.
|
||||
|
||||
@type bits: C{int}
|
||||
@rtype: C{tuple}
|
||||
"""
|
||||
primesKeys = self.primes.keys()
|
||||
primesKeys.sort(lambda x, y: cmp(abs(x - bits), abs(y - bits)))
|
||||
realBits = primesKeys[0]
|
||||
return random.choice(self.primes[realBits])
|
||||
|
||||
|
||||
def getService(self, transport, service):
|
||||
"""
|
||||
Return a class to use as a service for the given transport.
|
||||
|
||||
@type transport: L{transport.SSHServerTransport}
|
||||
@type service: C{str}
|
||||
@rtype: subclass of L{service.SSHService}
|
||||
"""
|
||||
if service == 'ssh-userauth' or hasattr(transport, 'avatar'):
|
||||
return self.services[service]
|
||||
|
|
@ -0,0 +1,934 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_filetransfer -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
import struct, errno
|
||||
|
||||
from twisted.internet import defer, protocol
|
||||
from twisted.python import failure, log
|
||||
|
||||
from common import NS, getNS
|
||||
from twisted.conch.interfaces import ISFTPServer, ISFTPFile
|
||||
|
||||
from zope import interface
|
||||
|
||||
|
||||
|
||||
class FileTransferBase(protocol.Protocol):
|
||||
|
||||
versions = (3, )
|
||||
|
||||
packetTypes = {}
|
||||
|
||||
def __init__(self):
|
||||
self.buf = ''
|
||||
self.otherVersion = None # this gets set
|
||||
|
||||
def sendPacket(self, kind, data):
|
||||
self.transport.write(struct.pack('!LB', len(data)+1, kind) + data)
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while len(self.buf) > 5:
|
||||
length, kind = struct.unpack('!LB', self.buf[:5])
|
||||
if len(self.buf) < 4 + length:
|
||||
return
|
||||
data, self.buf = self.buf[5:4+length], self.buf[4+length:]
|
||||
packetType = self.packetTypes.get(kind, None)
|
||||
if not packetType:
|
||||
log.msg('no packet type for', kind)
|
||||
continue
|
||||
f = getattr(self, 'packet_%s' % packetType, None)
|
||||
if not f:
|
||||
log.msg('not implemented: %s' % packetType)
|
||||
log.msg(repr(data[4:]))
|
||||
reqId, = struct.unpack('!L', data[:4])
|
||||
self._sendStatus(reqId, FX_OP_UNSUPPORTED,
|
||||
"don't understand %s" % packetType)
|
||||
#XXX not implemented
|
||||
continue
|
||||
try:
|
||||
f(data)
|
||||
except:
|
||||
log.err()
|
||||
continue
|
||||
reqId ,= struct.unpack('!L', data[:4])
|
||||
self._ebStatus(failure.Failure(e), reqId)
|
||||
|
||||
def _parseAttributes(self, data):
|
||||
flags ,= struct.unpack('!L', data[:4])
|
||||
attrs = {}
|
||||
data = data[4:]
|
||||
if flags & FILEXFER_ATTR_SIZE == FILEXFER_ATTR_SIZE:
|
||||
size ,= struct.unpack('!Q', data[:8])
|
||||
attrs['size'] = size
|
||||
data = data[8:]
|
||||
if flags & FILEXFER_ATTR_OWNERGROUP == FILEXFER_ATTR_OWNERGROUP:
|
||||
uid, gid = struct.unpack('!2L', data[:8])
|
||||
attrs['uid'] = uid
|
||||
attrs['gid'] = gid
|
||||
data = data[8:]
|
||||
if flags & FILEXFER_ATTR_PERMISSIONS == FILEXFER_ATTR_PERMISSIONS:
|
||||
perms ,= struct.unpack('!L', data[:4])
|
||||
attrs['permissions'] = perms
|
||||
data = data[4:]
|
||||
if flags & FILEXFER_ATTR_ACMODTIME == FILEXFER_ATTR_ACMODTIME:
|
||||
atime, mtime = struct.unpack('!2L', data[:8])
|
||||
attrs['atime'] = atime
|
||||
attrs['mtime'] = mtime
|
||||
data = data[8:]
|
||||
if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED:
|
||||
extended_count ,= struct.unpack('!L', data[:4])
|
||||
data = data[4:]
|
||||
for i in xrange(extended_count):
|
||||
extended_type, data = getNS(data)
|
||||
extended_data, data = getNS(data)
|
||||
attrs['ext_%s' % extended_type] = extended_data
|
||||
return attrs, data
|
||||
|
||||
def _packAttributes(self, attrs):
|
||||
flags = 0
|
||||
data = ''
|
||||
if 'size' in attrs:
|
||||
data += struct.pack('!Q', attrs['size'])
|
||||
flags |= FILEXFER_ATTR_SIZE
|
||||
if 'uid' in attrs and 'gid' in attrs:
|
||||
data += struct.pack('!2L', attrs['uid'], attrs['gid'])
|
||||
flags |= FILEXFER_ATTR_OWNERGROUP
|
||||
if 'permissions' in attrs:
|
||||
data += struct.pack('!L', attrs['permissions'])
|
||||
flags |= FILEXFER_ATTR_PERMISSIONS
|
||||
if 'atime' in attrs and 'mtime' in attrs:
|
||||
data += struct.pack('!2L', attrs['atime'], attrs['mtime'])
|
||||
flags |= FILEXFER_ATTR_ACMODTIME
|
||||
extended = []
|
||||
for k in attrs:
|
||||
if k.startswith('ext_'):
|
||||
ext_type = NS(k[4:])
|
||||
ext_data = NS(attrs[k])
|
||||
extended.append(ext_type+ext_data)
|
||||
if extended:
|
||||
data += struct.pack('!L', len(extended))
|
||||
data += ''.join(extended)
|
||||
flags |= FILEXFER_ATTR_EXTENDED
|
||||
return struct.pack('!L', flags) + data
|
||||
|
||||
class FileTransferServer(FileTransferBase):
|
||||
|
||||
def __init__(self, data=None, avatar=None):
|
||||
FileTransferBase.__init__(self)
|
||||
self.client = ISFTPServer(avatar) # yay interfaces
|
||||
self.openFiles = {}
|
||||
self.openDirs = {}
|
||||
|
||||
def packet_INIT(self, data):
|
||||
version ,= struct.unpack('!L', data[:4])
|
||||
self.version = min(list(self.versions) + [version])
|
||||
data = data[4:]
|
||||
ext = {}
|
||||
while data:
|
||||
ext_name, data = getNS(data)
|
||||
ext_data, data = getNS(data)
|
||||
ext[ext_name] = ext_data
|
||||
our_ext = self.client.gotVersion(version, ext)
|
||||
our_ext_data = ""
|
||||
for (k,v) in our_ext.items():
|
||||
our_ext_data += NS(k) + NS(v)
|
||||
self.sendPacket(FXP_VERSION, struct.pack('!L', self.version) + \
|
||||
our_ext_data)
|
||||
|
||||
def packet_OPEN(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
filename, data = getNS(data)
|
||||
flags ,= struct.unpack('!L', data[:4])
|
||||
data = data[4:]
|
||||
attrs, data = self._parseAttributes(data)
|
||||
assert data == '', 'still have data in OPEN: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.openFile, filename, flags, attrs)
|
||||
d.addCallback(self._cbOpenFile, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, "open failed")
|
||||
|
||||
def _cbOpenFile(self, fileObj, requestId):
|
||||
fileId = str(hash(fileObj))
|
||||
if fileId in self.openFiles:
|
||||
raise KeyError, 'id already open'
|
||||
self.openFiles[fileId] = fileObj
|
||||
self.sendPacket(FXP_HANDLE, requestId + NS(fileId))
|
||||
|
||||
def packet_CLOSE(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
handle, data = getNS(data)
|
||||
assert data == '', 'still have data in CLOSE: %s' % repr(data)
|
||||
if handle in self.openFiles:
|
||||
fileObj = self.openFiles[handle]
|
||||
d = defer.maybeDeferred(fileObj.close)
|
||||
d.addCallback(self._cbClose, handle, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, "close failed")
|
||||
elif handle in self.openDirs:
|
||||
dirObj = self.openDirs[handle][0]
|
||||
d = defer.maybeDeferred(dirObj.close)
|
||||
d.addCallback(self._cbClose, handle, requestId, 1)
|
||||
d.addErrback(self._ebStatus, requestId, "close failed")
|
||||
else:
|
||||
self._ebClose(failure.Failure(KeyError()), requestId)
|
||||
|
||||
def _cbClose(self, result, handle, requestId, isDir = 0):
|
||||
if isDir:
|
||||
del self.openDirs[handle]
|
||||
else:
|
||||
del self.openFiles[handle]
|
||||
self._sendStatus(requestId, FX_OK, 'file closed')
|
||||
|
||||
def packet_READ(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
handle, data = getNS(data)
|
||||
(offset, length), data = struct.unpack('!QL', data[:12]), data[12:]
|
||||
assert data == '', 'still have data in READ: %s' % repr(data)
|
||||
if handle not in self.openFiles:
|
||||
self._ebRead(failure.Failure(KeyError()), requestId)
|
||||
else:
|
||||
fileObj = self.openFiles[handle]
|
||||
d = defer.maybeDeferred(fileObj.readChunk, offset, length)
|
||||
d.addCallback(self._cbRead, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, "read failed")
|
||||
|
||||
def _cbRead(self, result, requestId):
|
||||
if result == '': # python's read will return this for EOF
|
||||
raise EOFError()
|
||||
self.sendPacket(FXP_DATA, requestId + NS(result))
|
||||
|
||||
def packet_WRITE(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
handle, data = getNS(data)
|
||||
offset, = struct.unpack('!Q', data[:8])
|
||||
data = data[8:]
|
||||
writeData, data = getNS(data)
|
||||
assert data == '', 'still have data in WRITE: %s' % repr(data)
|
||||
if handle not in self.openFiles:
|
||||
self._ebWrite(failure.Failure(KeyError()), requestId)
|
||||
else:
|
||||
fileObj = self.openFiles[handle]
|
||||
d = defer.maybeDeferred(fileObj.writeChunk, offset, writeData)
|
||||
d.addCallback(self._cbStatus, requestId, "write succeeded")
|
||||
d.addErrback(self._ebStatus, requestId, "write failed")
|
||||
|
||||
def packet_REMOVE(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
filename, data = getNS(data)
|
||||
assert data == '', 'still have data in REMOVE: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.removeFile, filename)
|
||||
d.addCallback(self._cbStatus, requestId, "remove succeeded")
|
||||
d.addErrback(self._ebStatus, requestId, "remove failed")
|
||||
|
||||
def packet_RENAME(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
oldPath, data = getNS(data)
|
||||
newPath, data = getNS(data)
|
||||
assert data == '', 'still have data in RENAME: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.renameFile, oldPath, newPath)
|
||||
d.addCallback(self._cbStatus, requestId, "rename succeeded")
|
||||
d.addErrback(self._ebStatus, requestId, "rename failed")
|
||||
|
||||
def packet_MKDIR(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
attrs, data = self._parseAttributes(data)
|
||||
assert data == '', 'still have data in MKDIR: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.makeDirectory, path, attrs)
|
||||
d.addCallback(self._cbStatus, requestId, "mkdir succeeded")
|
||||
d.addErrback(self._ebStatus, requestId, "mkdir failed")
|
||||
|
||||
def packet_RMDIR(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
assert data == '', 'still have data in RMDIR: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.removeDirectory, path)
|
||||
d.addCallback(self._cbStatus, requestId, "rmdir succeeded")
|
||||
d.addErrback(self._ebStatus, requestId, "rmdir failed")
|
||||
|
||||
def packet_OPENDIR(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
assert data == '', 'still have data in OPENDIR: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.openDirectory, path)
|
||||
d.addCallback(self._cbOpenDirectory, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, "opendir failed")
|
||||
|
||||
def _cbOpenDirectory(self, dirObj, requestId):
|
||||
handle = str(hash(dirObj))
|
||||
if handle in self.openDirs:
|
||||
raise KeyError, "already opened this directory"
|
||||
self.openDirs[handle] = [dirObj, iter(dirObj)]
|
||||
self.sendPacket(FXP_HANDLE, requestId + NS(handle))
|
||||
|
||||
def packet_READDIR(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
handle, data = getNS(data)
|
||||
assert data == '', 'still have data in READDIR: %s' % repr(data)
|
||||
if handle not in self.openDirs:
|
||||
self._ebStatus(failure.Failure(KeyError()), requestId)
|
||||
else:
|
||||
dirObj, dirIter = self.openDirs[handle]
|
||||
d = defer.maybeDeferred(self._scanDirectory, dirIter, [])
|
||||
d.addCallback(self._cbSendDirectory, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, "scan directory failed")
|
||||
|
||||
def _scanDirectory(self, dirIter, f):
|
||||
while len(f) < 250:
|
||||
try:
|
||||
info = dirIter.next()
|
||||
except StopIteration:
|
||||
if not f:
|
||||
raise EOFError
|
||||
return f
|
||||
if isinstance(info, defer.Deferred):
|
||||
info.addCallback(self._cbScanDirectory, dirIter, f)
|
||||
return
|
||||
else:
|
||||
f.append(info)
|
||||
return f
|
||||
|
||||
def _cbScanDirectory(self, result, dirIter, f):
|
||||
f.append(result)
|
||||
return self._scanDirectory(dirIter, f)
|
||||
|
||||
def _cbSendDirectory(self, result, requestId):
|
||||
data = ''
|
||||
for (filename, longname, attrs) in result:
|
||||
data += NS(filename)
|
||||
data += NS(longname)
|
||||
data += self._packAttributes(attrs)
|
||||
self.sendPacket(FXP_NAME, requestId +
|
||||
struct.pack('!L', len(result))+data)
|
||||
|
||||
def packet_STAT(self, data, followLinks = 1):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
assert data == '', 'still have data in STAT/LSTAT: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.getAttrs, path, followLinks)
|
||||
d.addCallback(self._cbStat, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, 'stat/lstat failed')
|
||||
|
||||
def packet_LSTAT(self, data):
|
||||
self.packet_STAT(data, 0)
|
||||
|
||||
def packet_FSTAT(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
handle, data = getNS(data)
|
||||
assert data == '', 'still have data in FSTAT: %s' % repr(data)
|
||||
if handle not in self.openFiles:
|
||||
self._ebStatus(failure.Failure(KeyError('%s not in self.openFiles'
|
||||
% handle)), requestId)
|
||||
else:
|
||||
fileObj = self.openFiles[handle]
|
||||
d = defer.maybeDeferred(fileObj.getAttrs)
|
||||
d.addCallback(self._cbStat, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, 'fstat failed')
|
||||
|
||||
def _cbStat(self, result, requestId):
|
||||
data = requestId + self._packAttributes(result)
|
||||
self.sendPacket(FXP_ATTRS, data)
|
||||
|
||||
def packet_SETSTAT(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
attrs, data = self._parseAttributes(data)
|
||||
if data != '':
|
||||
log.msg('WARN: still have data in SETSTAT: %s' % repr(data))
|
||||
d = defer.maybeDeferred(self.client.setAttrs, path, attrs)
|
||||
d.addCallback(self._cbStatus, requestId, 'setstat succeeded')
|
||||
d.addErrback(self._ebStatus, requestId, 'setstat failed')
|
||||
|
||||
def packet_FSETSTAT(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
handle, data = getNS(data)
|
||||
attrs, data = self._parseAttributes(data)
|
||||
assert data == '', 'still have data in FSETSTAT: %s' % repr(data)
|
||||
if handle not in self.openFiles:
|
||||
self._ebStatus(failure.Failure(KeyError()), requestId)
|
||||
else:
|
||||
fileObj = self.openFiles[handle]
|
||||
d = defer.maybeDeferred(fileObj.setAttrs, attrs)
|
||||
d.addCallback(self._cbStatus, requestId, 'fsetstat succeeded')
|
||||
d.addErrback(self._ebStatus, requestId, 'fsetstat failed')
|
||||
|
||||
def packet_READLINK(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
assert data == '', 'still have data in READLINK: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.readLink, path)
|
||||
d.addCallback(self._cbReadLink, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, 'readlink failed')
|
||||
|
||||
def _cbReadLink(self, result, requestId):
|
||||
self._cbSendDirectory([(result, '', {})], requestId)
|
||||
|
||||
def packet_SYMLINK(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
linkPath, data = getNS(data)
|
||||
targetPath, data = getNS(data)
|
||||
d = defer.maybeDeferred(self.client.makeLink, linkPath, targetPath)
|
||||
d.addCallback(self._cbStatus, requestId, 'symlink succeeded')
|
||||
d.addErrback(self._ebStatus, requestId, 'symlink failed')
|
||||
|
||||
def packet_REALPATH(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
path, data = getNS(data)
|
||||
assert data == '', 'still have data in REALPATH: %s' % repr(data)
|
||||
d = defer.maybeDeferred(self.client.realPath, path)
|
||||
d.addCallback(self._cbReadLink, requestId) # same return format
|
||||
d.addErrback(self._ebStatus, requestId, 'realpath failed')
|
||||
|
||||
def packet_EXTENDED(self, data):
|
||||
requestId = data[:4]
|
||||
data = data[4:]
|
||||
extName, extData = getNS(data)
|
||||
d = defer.maybeDeferred(self.client.extendedRequest, extName, extData)
|
||||
d.addCallback(self._cbExtended, requestId)
|
||||
d.addErrback(self._ebStatus, requestId, 'extended %s failed' % extName)
|
||||
|
||||
def _cbExtended(self, data, requestId):
|
||||
self.sendPacket(FXP_EXTENDED_REPLY, requestId + data)
|
||||
|
||||
def _cbStatus(self, result, requestId, msg = "request succeeded"):
|
||||
self._sendStatus(requestId, FX_OK, msg)
|
||||
|
||||
def _ebStatus(self, reason, requestId, msg = "request failed"):
|
||||
code = FX_FAILURE
|
||||
message = msg
|
||||
if reason.type in (IOError, OSError):
|
||||
if reason.value.errno == errno.ENOENT: # no such file
|
||||
code = FX_NO_SUCH_FILE
|
||||
message = reason.value.strerror
|
||||
elif reason.value.errno == errno.EACCES: # permission denied
|
||||
code = FX_PERMISSION_DENIED
|
||||
message = reason.value.strerror
|
||||
elif reason.value.errno == errno.EEXIST:
|
||||
code = FX_FILE_ALREADY_EXISTS
|
||||
else:
|
||||
log.err(reason)
|
||||
elif reason.type == EOFError: # EOF
|
||||
code = FX_EOF
|
||||
if reason.value.args:
|
||||
message = reason.value.args[0]
|
||||
elif reason.type == NotImplementedError:
|
||||
code = FX_OP_UNSUPPORTED
|
||||
if reason.value.args:
|
||||
message = reason.value.args[0]
|
||||
elif reason.type == SFTPError:
|
||||
code = reason.value.code
|
||||
message = reason.value.message
|
||||
else:
|
||||
log.err(reason)
|
||||
self._sendStatus(requestId, code, message)
|
||||
|
||||
def _sendStatus(self, requestId, code, message, lang = ''):
|
||||
"""
|
||||
Helper method to send a FXP_STATUS message.
|
||||
"""
|
||||
data = requestId + struct.pack('!L', code)
|
||||
data += NS(message)
|
||||
data += NS(lang)
|
||||
self.sendPacket(FXP_STATUS, data)
|
||||
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
Clean all opened files and directories.
|
||||
"""
|
||||
for fileObj in self.openFiles.values():
|
||||
fileObj.close()
|
||||
self.openFiles = {}
|
||||
for (dirObj, dirIter) in self.openDirs.values():
|
||||
dirObj.close()
|
||||
self.openDirs = {}
|
||||
|
||||
|
||||
|
||||
class FileTransferClient(FileTransferBase):
|
||||
|
||||
def __init__(self, extData = {}):
|
||||
"""
|
||||
@param extData: a dict of extended_name : extended_data items
|
||||
to be sent to the server.
|
||||
"""
|
||||
FileTransferBase.__init__(self)
|
||||
self.extData = {}
|
||||
self.counter = 0
|
||||
self.openRequests = {} # id -> Deferred
|
||||
self.wasAFile = {} # Deferred -> 1 TERRIBLE HACK
|
||||
|
||||
def connectionMade(self):
|
||||
data = struct.pack('!L', max(self.versions))
|
||||
for k,v in self.extData.itervalues():
|
||||
data += NS(k) + NS(v)
|
||||
self.sendPacket(FXP_INIT, data)
|
||||
|
||||
def _sendRequest(self, msg, data):
|
||||
data = struct.pack('!L', self.counter) + data
|
||||
d = defer.Deferred()
|
||||
self.openRequests[self.counter] = d
|
||||
self.counter += 1
|
||||
self.sendPacket(msg, data)
|
||||
return d
|
||||
|
||||
def _parseRequest(self, data):
|
||||
(id,) = struct.unpack('!L', data[:4])
|
||||
d = self.openRequests[id]
|
||||
del self.openRequests[id]
|
||||
return d, data[4:]
|
||||
|
||||
def openFile(self, filename, flags, attrs):
|
||||
"""
|
||||
Open a file.
|
||||
|
||||
This method returns a L{Deferred} that is called back with an object
|
||||
that provides the L{ISFTPFile} interface.
|
||||
|
||||
@param filename: a string representing the file to open.
|
||||
|
||||
@param flags: a integer of the flags to open the file with, ORed together.
|
||||
The flags and their values are listed at the bottom of this file.
|
||||
|
||||
@param attrs: a list of attributes to open the file with. It is a
|
||||
dictionary, consisting of 0 or more keys. The possible keys are::
|
||||
|
||||
size: the size of the file in bytes
|
||||
uid: the user ID of the file as an integer
|
||||
gid: the group ID of the file as an integer
|
||||
permissions: the permissions of the file with as an integer.
|
||||
the bit representation of this field is defined by POSIX.
|
||||
atime: the access time of the file as seconds since the epoch.
|
||||
mtime: the modification time of the file as seconds since the epoch.
|
||||
ext_*: extended attributes. The server is not required to
|
||||
understand this, but it may.
|
||||
|
||||
NOTE: there is no way to indicate text or binary files. it is up
|
||||
to the SFTP client to deal with this.
|
||||
"""
|
||||
data = NS(filename) + struct.pack('!L', flags) + self._packAttributes(attrs)
|
||||
d = self._sendRequest(FXP_OPEN, data)
|
||||
self.wasAFile[d] = (1, filename) # HACK
|
||||
return d
|
||||
|
||||
def removeFile(self, filename):
|
||||
"""
|
||||
Remove the given file.
|
||||
|
||||
This method returns a Deferred that is called back when it succeeds.
|
||||
|
||||
@param filename: the name of the file as a string.
|
||||
"""
|
||||
return self._sendRequest(FXP_REMOVE, NS(filename))
|
||||
|
||||
def renameFile(self, oldpath, newpath):
|
||||
"""
|
||||
Rename the given file.
|
||||
|
||||
This method returns a Deferred that is called back when it succeeds.
|
||||
|
||||
@param oldpath: the current location of the file.
|
||||
@param newpath: the new file name.
|
||||
"""
|
||||
return self._sendRequest(FXP_RENAME, NS(oldpath)+NS(newpath))
|
||||
|
||||
def makeDirectory(self, path, attrs):
|
||||
"""
|
||||
Make a directory.
|
||||
|
||||
This method returns a Deferred that is called back when it is
|
||||
created.
|
||||
|
||||
@param path: the name of the directory to create as a string.
|
||||
|
||||
@param attrs: a dictionary of attributes to create the directory
|
||||
with. Its meaning is the same as the attrs in the openFile method.
|
||||
"""
|
||||
return self._sendRequest(FXP_MKDIR, NS(path)+self._packAttributes(attrs))
|
||||
|
||||
def removeDirectory(self, path):
|
||||
"""
|
||||
Remove a directory (non-recursively)
|
||||
|
||||
It is an error to remove a directory that has files or directories in
|
||||
it.
|
||||
|
||||
This method returns a Deferred that is called back when it is removed.
|
||||
|
||||
@param path: the directory to remove.
|
||||
"""
|
||||
return self._sendRequest(FXP_RMDIR, NS(path))
|
||||
|
||||
def openDirectory(self, path):
|
||||
"""
|
||||
Open a directory for scanning.
|
||||
|
||||
This method returns a Deferred that is called back with an iterable
|
||||
object that has a close() method.
|
||||
|
||||
The close() method is called when the client is finished reading
|
||||
from the directory. At this point, the iterable will no longer
|
||||
be used.
|
||||
|
||||
The iterable returns triples of the form (filename, longname, attrs)
|
||||
or a Deferred that returns the same. The sequence must support
|
||||
__getitem__, but otherwise may be any 'sequence-like' object.
|
||||
|
||||
filename is the name of the file relative to the directory.
|
||||
logname is an expanded format of the filename. The recommended format
|
||||
is:
|
||||
-rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer
|
||||
1234567890 123 12345678 12345678 12345678 123456789012
|
||||
|
||||
The first line is sample output, the second is the length of the field.
|
||||
The fields are: permissions, link count, user owner, group owner,
|
||||
size in bytes, modification time.
|
||||
|
||||
attrs is a dictionary in the format of the attrs argument to openFile.
|
||||
|
||||
@param path: the directory to open.
|
||||
"""
|
||||
d = self._sendRequest(FXP_OPENDIR, NS(path))
|
||||
self.wasAFile[d] = (0, path)
|
||||
return d
|
||||
|
||||
def getAttrs(self, path, followLinks=0):
|
||||
"""
|
||||
Return the attributes for the given path.
|
||||
|
||||
This method returns a dictionary in the same format as the attrs
|
||||
argument to openFile or a Deferred that is called back with same.
|
||||
|
||||
@param path: the path to return attributes for as a string.
|
||||
@param followLinks: a boolean. if it is True, follow symbolic links
|
||||
and return attributes for the real path at the base. if it is False,
|
||||
return attributes for the specified path.
|
||||
"""
|
||||
if followLinks: m = FXP_STAT
|
||||
else: m = FXP_LSTAT
|
||||
return self._sendRequest(m, NS(path))
|
||||
|
||||
def setAttrs(self, path, attrs):
|
||||
"""
|
||||
Set the attributes for the path.
|
||||
|
||||
This method returns when the attributes are set or a Deferred that is
|
||||
called back when they are.
|
||||
|
||||
@param path: the path to set attributes for as a string.
|
||||
@param attrs: a dictionary in the same format as the attrs argument to
|
||||
openFile.
|
||||
"""
|
||||
data = NS(path) + self._packAttributes(attrs)
|
||||
return self._sendRequest(FXP_SETSTAT, data)
|
||||
|
||||
def readLink(self, path):
|
||||
"""
|
||||
Find the root of a set of symbolic links.
|
||||
|
||||
This method returns the target of the link, or a Deferred that
|
||||
returns the same.
|
||||
|
||||
@param path: the path of the symlink to read.
|
||||
"""
|
||||
d = self._sendRequest(FXP_READLINK, NS(path))
|
||||
return d.addCallback(self._cbRealPath)
|
||||
|
||||
def makeLink(self, linkPath, targetPath):
|
||||
"""
|
||||
Create a symbolic link.
|
||||
|
||||
This method returns when the link is made, or a Deferred that
|
||||
returns the same.
|
||||
|
||||
@param linkPath: the pathname of the symlink as a string
|
||||
@param targetPath: the path of the target of the link as a string.
|
||||
"""
|
||||
return self._sendRequest(FXP_SYMLINK, NS(linkPath)+NS(targetPath))
|
||||
|
||||
def realPath(self, path):
|
||||
"""
|
||||
Convert any path to an absolute path.
|
||||
|
||||
This method returns the absolute path as a string, or a Deferred
|
||||
that returns the same.
|
||||
|
||||
@param path: the path to convert as a string.
|
||||
"""
|
||||
d = self._sendRequest(FXP_REALPATH, NS(path))
|
||||
return d.addCallback(self._cbRealPath)
|
||||
|
||||
def _cbRealPath(self, result):
|
||||
name, longname, attrs = result[0]
|
||||
return name
|
||||
|
||||
def extendedRequest(self, request, data):
|
||||
"""
|
||||
Make an extended request of the server.
|
||||
|
||||
The method returns a Deferred that is called back with
|
||||
the result of the extended request.
|
||||
|
||||
@param request: the name of the extended request to make.
|
||||
@param data: any other data that goes along with the request.
|
||||
"""
|
||||
return self._sendRequest(FXP_EXTENDED, NS(request) + data)
|
||||
|
||||
def packet_VERSION(self, data):
|
||||
version, = struct.unpack('!L', data[:4])
|
||||
data = data[4:]
|
||||
d = {}
|
||||
while data:
|
||||
k, data = getNS(data)
|
||||
v, data = getNS(data)
|
||||
d[k]=v
|
||||
self.version = version
|
||||
self.gotServerVersion(version, d)
|
||||
|
||||
def packet_STATUS(self, data):
|
||||
d, data = self._parseRequest(data)
|
||||
code, = struct.unpack('!L', data[:4])
|
||||
data = data[4:]
|
||||
if len(data) >= 4:
|
||||
msg, data = getNS(data)
|
||||
if len(data) >= 4:
|
||||
lang, data = getNS(data)
|
||||
else:
|
||||
lang = ''
|
||||
else:
|
||||
msg = ''
|
||||
lang = ''
|
||||
if code == FX_OK:
|
||||
d.callback((msg, lang))
|
||||
elif code == FX_EOF:
|
||||
d.errback(EOFError(msg))
|
||||
elif code == FX_OP_UNSUPPORTED:
|
||||
d.errback(NotImplementedError(msg))
|
||||
else:
|
||||
d.errback(SFTPError(code, msg, lang))
|
||||
|
||||
def packet_HANDLE(self, data):
|
||||
d, data = self._parseRequest(data)
|
||||
isFile, name = self.wasAFile.pop(d)
|
||||
if isFile:
|
||||
cb = ClientFile(self, getNS(data)[0])
|
||||
else:
|
||||
cb = ClientDirectory(self, getNS(data)[0])
|
||||
cb.name = name
|
||||
d.callback(cb)
|
||||
|
||||
def packet_DATA(self, data):
|
||||
d, data = self._parseRequest(data)
|
||||
d.callback(getNS(data)[0])
|
||||
|
||||
def packet_NAME(self, data):
|
||||
d, data = self._parseRequest(data)
|
||||
count, = struct.unpack('!L', data[:4])
|
||||
data = data[4:]
|
||||
files = []
|
||||
for i in range(count):
|
||||
filename, data = getNS(data)
|
||||
longname, data = getNS(data)
|
||||
attrs, data = self._parseAttributes(data)
|
||||
files.append((filename, longname, attrs))
|
||||
d.callback(files)
|
||||
|
||||
def packet_ATTRS(self, data):
|
||||
d, data = self._parseRequest(data)
|
||||
d.callback(self._parseAttributes(data)[0])
|
||||
|
||||
def packet_EXTENDED_REPLY(self, data):
|
||||
d, data = self._parseRequest(data)
|
||||
d.callback(data)
|
||||
|
||||
def gotServerVersion(self, serverVersion, extData):
|
||||
"""
|
||||
Called when the client sends their version info.
|
||||
|
||||
@param otherVersion: an integer representing the version of the SFTP
|
||||
protocol they are claiming.
|
||||
@param extData: a dictionary of extended_name : extended_data items.
|
||||
These items are sent by the client to indicate additional features.
|
||||
"""
|
||||
|
||||
class ClientFile:
|
||||
|
||||
interface.implements(ISFTPFile)
|
||||
|
||||
def __init__(self, parent, handle):
|
||||
self.parent = parent
|
||||
self.handle = NS(handle)
|
||||
|
||||
def close(self):
|
||||
return self.parent._sendRequest(FXP_CLOSE, self.handle)
|
||||
|
||||
def readChunk(self, offset, length):
|
||||
data = self.handle + struct.pack("!QL", offset, length)
|
||||
return self.parent._sendRequest(FXP_READ, data)
|
||||
|
||||
def writeChunk(self, offset, chunk):
|
||||
data = self.handle + struct.pack("!Q", offset) + NS(chunk)
|
||||
return self.parent._sendRequest(FXP_WRITE, data)
|
||||
|
||||
def getAttrs(self):
|
||||
return self.parent._sendRequest(FXP_FSTAT, self.handle)
|
||||
|
||||
def setAttrs(self, attrs):
|
||||
data = self.handle + self.parent._packAttributes(attrs)
|
||||
return self.parent._sendRequest(FXP_FSTAT, data)
|
||||
|
||||
class ClientDirectory:
|
||||
|
||||
def __init__(self, parent, handle):
|
||||
self.parent = parent
|
||||
self.handle = NS(handle)
|
||||
self.filesCache = []
|
||||
|
||||
def read(self):
|
||||
d = self.parent._sendRequest(FXP_READDIR, self.handle)
|
||||
return d
|
||||
|
||||
def close(self):
|
||||
return self.parent._sendRequest(FXP_CLOSE, self.handle)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if self.filesCache:
|
||||
return self.filesCache.pop(0)
|
||||
d = self.read()
|
||||
d.addCallback(self._cbReadDir)
|
||||
d.addErrback(self._ebReadDir)
|
||||
return d
|
||||
|
||||
def _cbReadDir(self, names):
|
||||
self.filesCache = names[1:]
|
||||
return names[0]
|
||||
|
||||
def _ebReadDir(self, reason):
|
||||
reason.trap(EOFError)
|
||||
def _():
|
||||
raise StopIteration
|
||||
self.next = _
|
||||
return reason
|
||||
|
||||
|
||||
class SFTPError(Exception):
|
||||
|
||||
def __init__(self, errorCode, errorMessage, lang = ''):
|
||||
Exception.__init__(self)
|
||||
self.code = errorCode
|
||||
self._message = errorMessage
|
||||
self.lang = lang
|
||||
|
||||
|
||||
def message(self):
|
||||
"""
|
||||
A string received over the network that explains the error to a human.
|
||||
"""
|
||||
# Python 2.6 deprecates assigning to the 'message' attribute of an
|
||||
# exception. We define this read-only property here in order to
|
||||
# prevent the warning about deprecation while maintaining backwards
|
||||
# compatibility with object clients that rely on the 'message'
|
||||
# attribute being set correctly. See bug #3897.
|
||||
return self._message
|
||||
message = property(message)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return 'SFTPError %s: %s' % (self.code, self.message)
|
||||
|
||||
FXP_INIT = 1
|
||||
FXP_VERSION = 2
|
||||
FXP_OPEN = 3
|
||||
FXP_CLOSE = 4
|
||||
FXP_READ = 5
|
||||
FXP_WRITE = 6
|
||||
FXP_LSTAT = 7
|
||||
FXP_FSTAT = 8
|
||||
FXP_SETSTAT = 9
|
||||
FXP_FSETSTAT = 10
|
||||
FXP_OPENDIR = 11
|
||||
FXP_READDIR = 12
|
||||
FXP_REMOVE = 13
|
||||
FXP_MKDIR = 14
|
||||
FXP_RMDIR = 15
|
||||
FXP_REALPATH = 16
|
||||
FXP_STAT = 17
|
||||
FXP_RENAME = 18
|
||||
FXP_READLINK = 19
|
||||
FXP_SYMLINK = 20
|
||||
FXP_STATUS = 101
|
||||
FXP_HANDLE = 102
|
||||
FXP_DATA = 103
|
||||
FXP_NAME = 104
|
||||
FXP_ATTRS = 105
|
||||
FXP_EXTENDED = 200
|
||||
FXP_EXTENDED_REPLY = 201
|
||||
|
||||
FILEXFER_ATTR_SIZE = 0x00000001
|
||||
FILEXFER_ATTR_UIDGID = 0x00000002
|
||||
FILEXFER_ATTR_OWNERGROUP = FILEXFER_ATTR_UIDGID
|
||||
FILEXFER_ATTR_PERMISSIONS = 0x00000004
|
||||
FILEXFER_ATTR_ACMODTIME = 0x00000008
|
||||
FILEXFER_ATTR_EXTENDED = 0x80000000L
|
||||
|
||||
FILEXFER_TYPE_REGULAR = 1
|
||||
FILEXFER_TYPE_DIRECTORY = 2
|
||||
FILEXFER_TYPE_SYMLINK = 3
|
||||
FILEXFER_TYPE_SPECIAL = 4
|
||||
FILEXFER_TYPE_UNKNOWN = 5
|
||||
|
||||
FXF_READ = 0x00000001
|
||||
FXF_WRITE = 0x00000002
|
||||
FXF_APPEND = 0x00000004
|
||||
FXF_CREAT = 0x00000008
|
||||
FXF_TRUNC = 0x00000010
|
||||
FXF_EXCL = 0x00000020
|
||||
FXF_TEXT = 0x00000040
|
||||
|
||||
FX_OK = 0
|
||||
FX_EOF = 1
|
||||
FX_NO_SUCH_FILE = 2
|
||||
FX_PERMISSION_DENIED = 3
|
||||
FX_FAILURE = 4
|
||||
FX_BAD_MESSAGE = 5
|
||||
FX_NO_CONNECTION = 6
|
||||
FX_CONNECTION_LOST = 7
|
||||
FX_OP_UNSUPPORTED = 8
|
||||
FX_FILE_ALREADY_EXISTS = 11
|
||||
# http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more
|
||||
# useful error codes, but so far OpenSSH doesn't implement them. We use them
|
||||
# internally for clarity, but for now define them all as FX_FAILURE to be
|
||||
# compatible with existing software.
|
||||
FX_NOT_A_DIRECTORY = FX_FAILURE
|
||||
FX_FILE_IS_A_DIRECTORY = FX_FAILURE
|
||||
|
||||
|
||||
# initialize FileTransferBase.packetTypes:
|
||||
g = globals()
|
||||
for name in g.keys():
|
||||
if name.startswith('FXP_'):
|
||||
value = g[name]
|
||||
FileTransferBase.packetTypes[value] = name[4:]
|
||||
del g, name, value
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""
|
||||
This module contains the implementation of the TCP forwarding, which allows
|
||||
clients and servers to forward arbitrary TCP data across the connection.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.python import log
|
||||
|
||||
import common, channel
|
||||
|
||||
class SSHListenForwardingFactory(protocol.Factory):
|
||||
def __init__(self, connection, hostport, klass):
|
||||
self.conn = connection
|
||||
self.hostport = hostport # tuple
|
||||
self.klass = klass
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
channel = self.klass(conn = self.conn)
|
||||
client = SSHForwardingClient(channel)
|
||||
channel.client = client
|
||||
addrTuple = (addr.host, addr.port)
|
||||
channelOpenData = packOpen_direct_tcpip(self.hostport, addrTuple)
|
||||
self.conn.openChannel(channel, channelOpenData)
|
||||
return client
|
||||
|
||||
class SSHListenForwardingChannel(channel.SSHChannel):
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
log.msg('opened forwarding channel %s' % self.id)
|
||||
if len(self.client.buf)>1:
|
||||
b = self.client.buf[1:]
|
||||
self.write(b)
|
||||
self.client.buf = ''
|
||||
|
||||
def openFailed(self, reason):
|
||||
self.closed()
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.client.transport.write(data)
|
||||
|
||||
def eofReceived(self):
|
||||
self.client.transport.loseConnection()
|
||||
|
||||
def closed(self):
|
||||
if hasattr(self, 'client'):
|
||||
log.msg('closing local forwarding channel %s' % self.id)
|
||||
self.client.transport.loseConnection()
|
||||
del self.client
|
||||
|
||||
class SSHListenClientForwardingChannel(SSHListenForwardingChannel):
|
||||
|
||||
name = 'direct-tcpip'
|
||||
|
||||
class SSHListenServerForwardingChannel(SSHListenForwardingChannel):
|
||||
|
||||
name = 'forwarded-tcpip'
|
||||
|
||||
class SSHConnectForwardingChannel(channel.SSHChannel):
|
||||
|
||||
def __init__(self, hostport, *args, **kw):
|
||||
channel.SSHChannel.__init__(self, *args, **kw)
|
||||
self.hostport = hostport
|
||||
self.client = None
|
||||
self.clientBuf = ''
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
cc = protocol.ClientCreator(reactor, SSHForwardingClient, self)
|
||||
log.msg("connecting to %s:%i" % self.hostport)
|
||||
cc.connectTCP(*self.hostport).addCallbacks(self._setClient, self._close)
|
||||
|
||||
def _setClient(self, client):
|
||||
self.client = client
|
||||
log.msg("connected to %s:%i" % self.hostport)
|
||||
if self.clientBuf:
|
||||
self.client.transport.write(self.clientBuf)
|
||||
self.clientBuf = None
|
||||
if self.client.buf[1:]:
|
||||
self.write(self.client.buf[1:])
|
||||
self.client.buf = ''
|
||||
|
||||
def _close(self, reason):
|
||||
log.msg("failed to connect: %s" % reason)
|
||||
self.loseConnection()
|
||||
|
||||
def dataReceived(self, data):
|
||||
if self.client:
|
||||
self.client.transport.write(data)
|
||||
else:
|
||||
self.clientBuf += data
|
||||
|
||||
def closed(self):
|
||||
if self.client:
|
||||
log.msg('closed remote forwarding channel %s' % self.id)
|
||||
if self.client.channel:
|
||||
self.loseConnection()
|
||||
self.client.transport.loseConnection()
|
||||
del self.client
|
||||
|
||||
def openConnectForwardingClient(remoteWindow, remoteMaxPacket, data, avatar):
|
||||
remoteHP, origHP = unpackOpen_direct_tcpip(data)
|
||||
return SSHConnectForwardingChannel(remoteHP,
|
||||
remoteWindow=remoteWindow,
|
||||
remoteMaxPacket=remoteMaxPacket,
|
||||
avatar=avatar)
|
||||
|
||||
class SSHForwardingClient(protocol.Protocol):
|
||||
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
self.buf = '\000'
|
||||
|
||||
def dataReceived(self, data):
|
||||
if self.buf:
|
||||
self.buf += data
|
||||
else:
|
||||
self.channel.write(data)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.channel:
|
||||
self.channel.loseConnection()
|
||||
self.channel = None
|
||||
|
||||
|
||||
def packOpen_direct_tcpip((connHost, connPort), (origHost, origPort)):
|
||||
"""Pack the data suitable for sending in a CHANNEL_OPEN packet.
|
||||
"""
|
||||
conn = common.NS(connHost) + struct.pack('>L', connPort)
|
||||
orig = common.NS(origHost) + struct.pack('>L', origPort)
|
||||
return conn + orig
|
||||
|
||||
packOpen_forwarded_tcpip = packOpen_direct_tcpip
|
||||
|
||||
def unpackOpen_direct_tcpip(data):
|
||||
"""Unpack the data to a usable format.
|
||||
"""
|
||||
connHost, rest = common.getNS(data)
|
||||
connPort = int(struct.unpack('>L', rest[:4])[0])
|
||||
origHost, rest = common.getNS(rest[4:])
|
||||
origPort = int(struct.unpack('>L', rest[:4])[0])
|
||||
return (connHost, connPort), (origHost, origPort)
|
||||
|
||||
unpackOpen_forwarded_tcpip = unpackOpen_direct_tcpip
|
||||
|
||||
def packGlobal_tcpip_forward((host, port)):
|
||||
return common.NS(host) + struct.pack('>L', port)
|
||||
|
||||
def unpackGlobal_tcpip_forward(data):
|
||||
host, rest = common.getNS(data)
|
||||
port = int(struct.unpack('>L', rest[:4])[0])
|
||||
return host, port
|
||||
|
||||
"""This is how the data -> eof -> close stuff /should/ work.
|
||||
|
||||
debug3: channel 1: waiting for connection
|
||||
debug1: channel 1: connected
|
||||
debug1: channel 1: read<=0 rfd 7 len 0
|
||||
debug1: channel 1: read failed
|
||||
debug1: channel 1: close_read
|
||||
debug1: channel 1: input open -> drain
|
||||
debug1: channel 1: ibuf empty
|
||||
debug1: channel 1: send eof
|
||||
debug1: channel 1: input drain -> closed
|
||||
debug1: channel 1: rcvd eof
|
||||
debug1: channel 1: output open -> drain
|
||||
debug1: channel 1: obuf empty
|
||||
debug1: channel 1: close_write
|
||||
debug1: channel 1: output drain -> closed
|
||||
debug1: channel 1: rcvd close
|
||||
debug3: channel 1: will not send data after close
|
||||
debug1: channel 1: send close
|
||||
debug1: channel 1: is dead
|
||||
"""
|
||||
857
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/keys.py
Normal file
857
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/keys.py
Normal file
|
|
@ -0,0 +1,857 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_keys -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Handling of RSA and DSA keys.
|
||||
|
||||
Maintainer: U{Paul Swartz}
|
||||
"""
|
||||
|
||||
# base library imports
|
||||
import base64
|
||||
import itertools
|
||||
from hashlib import md5, sha1
|
||||
|
||||
# external library imports
|
||||
from Crypto.Cipher import DES3, AES
|
||||
from Crypto.PublicKey import RSA, DSA
|
||||
from Crypto import Util
|
||||
from pyasn1.error import PyAsn1Error
|
||||
from pyasn1.type import univ
|
||||
from pyasn1.codec.ber import decoder as berDecoder
|
||||
from pyasn1.codec.ber import encoder as berEncoder
|
||||
|
||||
# twisted
|
||||
from twisted.python import randbytes
|
||||
|
||||
# sibling imports
|
||||
from twisted.conch.ssh import common, sexpy
|
||||
|
||||
|
||||
|
||||
class BadKeyError(Exception):
|
||||
"""
|
||||
Raised when a key isn't what we expected from it.
|
||||
|
||||
XXX: we really need to check for bad keys
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class EncryptedKeyError(Exception):
|
||||
"""
|
||||
Raised when an encrypted key is presented to fromString/fromFile without
|
||||
a password.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class Key(object):
|
||||
"""
|
||||
An object representing a key. A key can be either a public or
|
||||
private key. A public key can verify a signature; a private key can
|
||||
create or verify a signature. To generate a string that can be stored
|
||||
on disk, use the toString method. If you have a private key, but want
|
||||
the string representation of the public key, use Key.public().toString().
|
||||
|
||||
@ivar keyObject: The C{Crypto.PublicKey.pubkey.pubkey} object that
|
||||
operations are performed with.
|
||||
"""
|
||||
|
||||
def fromFile(Class, filename, type=None, passphrase=None):
|
||||
"""
|
||||
Return a Key object corresponding to the data in filename. type
|
||||
and passphrase function as they do in fromString.
|
||||
"""
|
||||
return Class.fromString(file(filename, 'rb').read(), type, passphrase)
|
||||
fromFile = classmethod(fromFile)
|
||||
|
||||
|
||||
def fromString(Class, data, type=None, passphrase=None):
|
||||
"""
|
||||
Return a Key object corresponding to the string data.
|
||||
type is optionally the type of string, matching a _fromString_*
|
||||
method. Otherwise, the _guessStringType() classmethod will be used
|
||||
to guess a type. If the key is encrypted, passphrase is used as
|
||||
the decryption key.
|
||||
|
||||
@type data: C{str}
|
||||
@type type: C{None}/C{str}
|
||||
@type passphrase: C{None}/C{str}
|
||||
@rtype: C{Key}
|
||||
"""
|
||||
if type is None:
|
||||
type = Class._guessStringType(data)
|
||||
if type is None:
|
||||
raise BadKeyError('cannot guess the type of %r' % data)
|
||||
method = getattr(Class, '_fromString_%s' % type.upper(), None)
|
||||
if method is None:
|
||||
raise BadKeyError('no _fromString method for %s' % type)
|
||||
if method.func_code.co_argcount == 2: # no passphrase
|
||||
if passphrase:
|
||||
raise BadKeyError('key not encrypted')
|
||||
return method(data)
|
||||
else:
|
||||
return method(data, passphrase)
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
|
||||
def _fromString_BLOB(Class, blob):
|
||||
"""
|
||||
Return a public key object corresponding to this public key blob.
|
||||
The format of a RSA public key blob is::
|
||||
string 'ssh-rsa'
|
||||
integer e
|
||||
integer n
|
||||
|
||||
The format of a DSA public key blob is::
|
||||
string 'ssh-dss'
|
||||
integer p
|
||||
integer q
|
||||
integer g
|
||||
integer y
|
||||
|
||||
@type blob: C{str}
|
||||
@return: a C{Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if the key type (the first string) is unknown.
|
||||
"""
|
||||
keyType, rest = common.getNS(blob)
|
||||
if keyType == 'ssh-rsa':
|
||||
e, n, rest = common.getMP(rest, 2)
|
||||
return Class(RSA.construct((n, e)))
|
||||
elif keyType == 'ssh-dss':
|
||||
p, q, g, y, rest = common.getMP(rest, 4)
|
||||
return Class(DSA.construct((y, g, p, q)))
|
||||
else:
|
||||
raise BadKeyError('unknown blob type: %s' % keyType)
|
||||
_fromString_BLOB = classmethod(_fromString_BLOB)
|
||||
|
||||
|
||||
def _fromString_PRIVATE_BLOB(Class, blob):
|
||||
"""
|
||||
Return a private key object corresponding to this private key blob.
|
||||
The blob formats are as follows:
|
||||
|
||||
RSA keys::
|
||||
string 'ssh-rsa'
|
||||
integer n
|
||||
integer e
|
||||
integer d
|
||||
integer u
|
||||
integer p
|
||||
integer q
|
||||
|
||||
DSA keys::
|
||||
string 'ssh-dss'
|
||||
integer p
|
||||
integer q
|
||||
integer g
|
||||
integer y
|
||||
integer x
|
||||
|
||||
@type blob: C{str}
|
||||
@return: a C{Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if the key type (the first string) is unknown.
|
||||
"""
|
||||
keyType, rest = common.getNS(blob)
|
||||
|
||||
if keyType == 'ssh-rsa':
|
||||
n, e, d, u, p, q, rest = common.getMP(rest, 6)
|
||||
rsakey = Class(RSA.construct((n, e, d, p, q, u)))
|
||||
return rsakey
|
||||
elif keyType == 'ssh-dss':
|
||||
p, q, g, y, x, rest = common.getMP(rest, 5)
|
||||
dsakey = Class(DSA.construct((y, g, p, q, x)))
|
||||
return dsakey
|
||||
else:
|
||||
raise BadKeyError('unknown blob type: %s' % keyType)
|
||||
_fromString_PRIVATE_BLOB = classmethod(_fromString_PRIVATE_BLOB)
|
||||
|
||||
|
||||
def _fromString_PUBLIC_OPENSSH(Class, data):
|
||||
"""
|
||||
Return a public key object corresponding to this OpenSSH public key
|
||||
string. The format of an OpenSSH public key string is::
|
||||
<key type> <base64-encoded public key blob>
|
||||
|
||||
@type data: C{str}
|
||||
@return: A {Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if the blob type is unknown.
|
||||
"""
|
||||
blob = base64.decodestring(data.split()[1])
|
||||
return Class._fromString_BLOB(blob)
|
||||
_fromString_PUBLIC_OPENSSH = classmethod(_fromString_PUBLIC_OPENSSH)
|
||||
|
||||
|
||||
def _fromString_PRIVATE_OPENSSH(Class, data, passphrase):
|
||||
"""
|
||||
Return a private key object corresponding to this OpenSSH private key
|
||||
string. If the key is encrypted, passphrase MUST be provided.
|
||||
Providing a passphrase for an unencrypted key is an error.
|
||||
|
||||
The format of an OpenSSH private key string is::
|
||||
-----BEGIN <key type> PRIVATE KEY-----
|
||||
[Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,<initialization value>]
|
||||
<base64-encoded ASN.1 structure>
|
||||
------END <key type> PRIVATE KEY------
|
||||
|
||||
The ASN.1 structure of a RSA key is::
|
||||
(0, n, e, d, p, q)
|
||||
|
||||
The ASN.1 structure of a DSA key is::
|
||||
(0, p, q, g, y, x)
|
||||
|
||||
@type data: C{str}
|
||||
@type passphrase: C{str}
|
||||
@return: a C{Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if
|
||||
* a passphrase is provided for an unencrypted key
|
||||
* the ASN.1 encoding is incorrect
|
||||
@raises EncryptedKeyError: if
|
||||
* a passphrase is not provided for an encrypted key
|
||||
"""
|
||||
lines = data.strip().split('\n')
|
||||
kind = lines[0][11:14]
|
||||
if lines[1].startswith('Proc-Type: 4,ENCRYPTED'): # encrypted key
|
||||
if not passphrase:
|
||||
raise EncryptedKeyError('Passphrase must be provided '
|
||||
'for an encrypted key')
|
||||
|
||||
# Determine cipher and initialization vector
|
||||
try:
|
||||
_, cipher_iv_info = lines[2].split(' ', 1)
|
||||
cipher, ivdata = cipher_iv_info.rstrip().split(',', 1)
|
||||
except ValueError:
|
||||
raise BadKeyError('invalid DEK-info %r' % lines[2])
|
||||
|
||||
if cipher == 'AES-128-CBC':
|
||||
CipherClass = AES
|
||||
keySize = 16
|
||||
if len(ivdata) != 32:
|
||||
raise BadKeyError('AES encrypted key with a bad IV')
|
||||
elif cipher == 'DES-EDE3-CBC':
|
||||
CipherClass = DES3
|
||||
keySize = 24
|
||||
if len(ivdata) != 16:
|
||||
raise BadKeyError('DES encrypted key with a bad IV')
|
||||
else:
|
||||
raise BadKeyError('unknown encryption type %r' % cipher)
|
||||
|
||||
# extract keyData for decoding
|
||||
iv = ''.join([chr(int(ivdata[i:i + 2], 16))
|
||||
for i in range(0, len(ivdata), 2)])
|
||||
ba = md5(passphrase + iv[:8]).digest()
|
||||
bb = md5(ba + passphrase + iv[:8]).digest()
|
||||
decKey = (ba + bb)[:keySize]
|
||||
b64Data = base64.decodestring(''.join(lines[3:-1]))
|
||||
keyData = CipherClass.new(decKey,
|
||||
CipherClass.MODE_CBC,
|
||||
iv).decrypt(b64Data)
|
||||
removeLen = ord(keyData[-1])
|
||||
keyData = keyData[:-removeLen]
|
||||
else:
|
||||
b64Data = ''.join(lines[1:-1])
|
||||
keyData = base64.decodestring(b64Data)
|
||||
|
||||
try:
|
||||
decodedKey = berDecoder.decode(keyData)[0]
|
||||
except PyAsn1Error, e:
|
||||
raise BadKeyError('Failed to decode key (Bad Passphrase?): %s' % e)
|
||||
|
||||
if kind == 'RSA':
|
||||
if len(decodedKey) == 2: # alternate RSA key
|
||||
decodedKey = decodedKey[0]
|
||||
if len(decodedKey) < 6:
|
||||
raise BadKeyError('RSA key failed to decode properly')
|
||||
|
||||
n, e, d, p, q = [long(value) for value in decodedKey[1:6]]
|
||||
if p > q: # make p smaller than q
|
||||
p, q = q, p
|
||||
return Class(RSA.construct((n, e, d, p, q)))
|
||||
elif kind == 'DSA':
|
||||
p, q, g, y, x = [long(value) for value in decodedKey[1: 6]]
|
||||
if len(decodedKey) < 6:
|
||||
raise BadKeyError('DSA key failed to decode properly')
|
||||
return Class(DSA.construct((y, g, p, q, x)))
|
||||
_fromString_PRIVATE_OPENSSH = classmethod(_fromString_PRIVATE_OPENSSH)
|
||||
|
||||
|
||||
def _fromString_PUBLIC_LSH(Class, data):
|
||||
"""
|
||||
Return a public key corresponding to this LSH public key string.
|
||||
The LSH public key string format is::
|
||||
<s-expression: ('public-key', (<key type>, (<name, <value>)+))>
|
||||
|
||||
The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e.
|
||||
The names for a DSA (key type 'dsa') key are: y, g, p, q.
|
||||
|
||||
@type data: C{str}
|
||||
@return: a C{Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if the key type is unknown
|
||||
"""
|
||||
sexp = sexpy.parse(base64.decodestring(data[1:-1]))
|
||||
assert sexp[0] == 'public-key'
|
||||
kd = {}
|
||||
for name, data in sexp[1][1:]:
|
||||
kd[name] = common.getMP(common.NS(data))[0]
|
||||
if sexp[1][0] == 'dsa':
|
||||
return Class(DSA.construct((kd['y'], kd['g'], kd['p'], kd['q'])))
|
||||
elif sexp[1][0] == 'rsa-pkcs1-sha1':
|
||||
return Class(RSA.construct((kd['n'], kd['e'])))
|
||||
else:
|
||||
raise BadKeyError('unknown lsh key type %s' % sexp[1][0])
|
||||
_fromString_PUBLIC_LSH = classmethod(_fromString_PUBLIC_LSH)
|
||||
|
||||
|
||||
def _fromString_PRIVATE_LSH(Class, data):
|
||||
"""
|
||||
Return a private key corresponding to this LSH private key string.
|
||||
The LSH private key string format is::
|
||||
<s-expression: ('private-key', (<key type>, (<name>, <value>)+))>
|
||||
|
||||
The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q.
|
||||
The names for a DSA (key type 'dsa') key are: y, g, p, q, x.
|
||||
|
||||
@type data: C{str}
|
||||
@return: a {Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if the key type is unknown
|
||||
"""
|
||||
sexp = sexpy.parse(data)
|
||||
assert sexp[0] == 'private-key'
|
||||
kd = {}
|
||||
for name, data in sexp[1][1:]:
|
||||
kd[name] = common.getMP(common.NS(data))[0]
|
||||
if sexp[1][0] == 'dsa':
|
||||
assert len(kd) == 5, len(kd)
|
||||
return Class(DSA.construct((kd['y'], kd['g'], kd['p'],
|
||||
kd['q'], kd['x'])))
|
||||
elif sexp[1][0] == 'rsa-pkcs1':
|
||||
assert len(kd) == 8, len(kd)
|
||||
if kd['p'] > kd['q']: # make p smaller than q
|
||||
kd['p'], kd['q'] = kd['q'], kd['p']
|
||||
return Class(RSA.construct((kd['n'], kd['e'], kd['d'],
|
||||
kd['p'], kd['q'])))
|
||||
else:
|
||||
raise BadKeyError('unknown lsh key type %s' % sexp[1][0])
|
||||
_fromString_PRIVATE_LSH = classmethod(_fromString_PRIVATE_LSH)
|
||||
|
||||
|
||||
def _fromString_AGENTV3(Class, data):
|
||||
"""
|
||||
Return a private key object corresponsing to the Secure Shell Key
|
||||
Agent v3 format.
|
||||
|
||||
The SSH Key Agent v3 format for a RSA key is::
|
||||
string 'ssh-rsa'
|
||||
integer e
|
||||
integer d
|
||||
integer n
|
||||
integer u
|
||||
integer p
|
||||
integer q
|
||||
|
||||
The SSH Key Agent v3 format for a DSA key is::
|
||||
string 'ssh-dss'
|
||||
integer p
|
||||
integer q
|
||||
integer g
|
||||
integer y
|
||||
integer x
|
||||
|
||||
@type data: C{str}
|
||||
@return: a C{Crypto.PublicKey.pubkey.pubkey} object
|
||||
@raises BadKeyError: if the key type (the first string) is unknown
|
||||
"""
|
||||
keyType, data = common.getNS(data)
|
||||
if keyType == 'ssh-dss':
|
||||
p, data = common.getMP(data)
|
||||
q, data = common.getMP(data)
|
||||
g, data = common.getMP(data)
|
||||
y, data = common.getMP(data)
|
||||
x, data = common.getMP(data)
|
||||
return Class(DSA.construct((y, g, p, q, x)))
|
||||
elif keyType == 'ssh-rsa':
|
||||
e, data = common.getMP(data)
|
||||
d, data = common.getMP(data)
|
||||
n, data = common.getMP(data)
|
||||
u, data = common.getMP(data)
|
||||
p, data = common.getMP(data)
|
||||
q, data = common.getMP(data)
|
||||
return Class(RSA.construct((n, e, d, p, q, u)))
|
||||
else:
|
||||
raise BadKeyError("unknown key type %s" % keyType)
|
||||
_fromString_AGENTV3 = classmethod(_fromString_AGENTV3)
|
||||
|
||||
|
||||
def _guessStringType(Class, data):
|
||||
"""
|
||||
Guess the type of key in data. The types map to _fromString_*
|
||||
methods.
|
||||
"""
|
||||
if data.startswith('ssh-'):
|
||||
return 'public_openssh'
|
||||
elif data.startswith('-----BEGIN'):
|
||||
return 'private_openssh'
|
||||
elif data.startswith('{'):
|
||||
return 'public_lsh'
|
||||
elif data.startswith('('):
|
||||
return 'private_lsh'
|
||||
elif data.startswith('\x00\x00\x00\x07ssh-'):
|
||||
ignored, rest = common.getNS(data)
|
||||
count = 0
|
||||
while rest:
|
||||
count += 1
|
||||
ignored, rest = common.getMP(rest)
|
||||
if count > 4:
|
||||
return 'agentv3'
|
||||
else:
|
||||
return 'blob'
|
||||
_guessStringType = classmethod(_guessStringType)
|
||||
|
||||
|
||||
def __init__(self, keyObject):
|
||||
"""
|
||||
Initialize a PublicKey with a C{Crypto.PublicKey.pubkey.pubkey}
|
||||
object.
|
||||
|
||||
@type keyObject: C{Crypto.PublicKey.pubkey.pubkey}
|
||||
"""
|
||||
self.keyObject = keyObject
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Return True if other represents an object with the same key.
|
||||
"""
|
||||
if type(self) == type(other):
|
||||
return self.type() == other.type() and self.data() == other.data()
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
"""
|
||||
Return True if other represents anything other than this key.
|
||||
"""
|
||||
result = self.__eq__(other)
|
||||
if result == NotImplemented:
|
||||
return result
|
||||
return not result
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return a pretty representation of this object.
|
||||
"""
|
||||
lines = [
|
||||
'<%s %s (%s bits)' % (
|
||||
self.type(),
|
||||
self.isPublic() and 'Public Key' or 'Private Key',
|
||||
self.keyObject.size())]
|
||||
for k, v in sorted(self.data().items()):
|
||||
lines.append('attr %s:' % k)
|
||||
by = common.MP(v)[4:]
|
||||
while by:
|
||||
m = by[:15]
|
||||
by = by[15:]
|
||||
o = ''
|
||||
for c in m:
|
||||
o = o + '%02x:' % ord(c)
|
||||
if len(m) < 15:
|
||||
o = o[:-1]
|
||||
lines.append('\t' + o)
|
||||
lines[-1] = lines[-1] + '>'
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def isPublic(self):
|
||||
"""
|
||||
Returns True if this Key is a public key.
|
||||
"""
|
||||
return not self.keyObject.has_private()
|
||||
|
||||
|
||||
def public(self):
|
||||
"""
|
||||
Returns a version of this key containing only the public key data.
|
||||
If this is a public key, this may or may not be the same object
|
||||
as self.
|
||||
"""
|
||||
return Key(self.keyObject.publickey())
|
||||
|
||||
|
||||
def fingerprint(self):
|
||||
"""
|
||||
Get the user presentation of the fingerprint of this L{Key}. As
|
||||
described by U{RFC 4716 section
|
||||
4<http://tools.ietf.org/html/rfc4716#section-4>}::
|
||||
|
||||
The fingerprint of a public key consists of the output of the MD5
|
||||
message-digest algorithm [RFC1321]. The input to the algorithm is
|
||||
the public key data as specified by [RFC4253]. (...) The output
|
||||
of the (MD5) algorithm is presented to the user as a sequence of 16
|
||||
octets printed as hexadecimal with lowercase letters and separated
|
||||
by colons.
|
||||
|
||||
@since: 8.2
|
||||
|
||||
@return: the user presentation of this L{Key}'s fingerprint, as a
|
||||
string.
|
||||
|
||||
@rtype: L{str}
|
||||
"""
|
||||
return ':'.join([x.encode('hex') for x in md5(self.blob()).digest()])
|
||||
|
||||
|
||||
def type(self):
|
||||
"""
|
||||
Return the type of the object we wrap. Currently this can only be
|
||||
'RSA' or 'DSA'.
|
||||
"""
|
||||
# the class is Crypto.PublicKey.<type>.<stuff we don't care about>
|
||||
mod = self.keyObject.__class__.__module__
|
||||
if mod.startswith('Crypto.PublicKey'):
|
||||
type = mod.split('.')[2]
|
||||
else:
|
||||
raise RuntimeError('unknown type of object: %r' % self.keyObject)
|
||||
if type in ('RSA', 'DSA'):
|
||||
return type
|
||||
else:
|
||||
raise RuntimeError('unknown type of key: %s' % type)
|
||||
|
||||
|
||||
def sshType(self):
|
||||
"""
|
||||
Return the type of the object we wrap as defined in the ssh protocol.
|
||||
Currently this can only be 'ssh-rsa' or 'ssh-dss'.
|
||||
"""
|
||||
return {'RSA': 'ssh-rsa', 'DSA': 'ssh-dss'}[self.type()]
|
||||
|
||||
|
||||
def data(self):
|
||||
"""
|
||||
Return the values of the public key as a dictionary.
|
||||
|
||||
@rtype: C{dict}
|
||||
"""
|
||||
keyData = {}
|
||||
for name in self.keyObject.keydata:
|
||||
value = getattr(self.keyObject, name, None)
|
||||
if value is not None:
|
||||
keyData[name] = value
|
||||
return keyData
|
||||
|
||||
|
||||
def blob(self):
|
||||
"""
|
||||
Return the public key blob for this key. The blob is the
|
||||
over-the-wire format for public keys:
|
||||
|
||||
RSA keys::
|
||||
string 'ssh-rsa'
|
||||
integer e
|
||||
integer n
|
||||
|
||||
DSA keys::
|
||||
string 'ssh-dss'
|
||||
integer p
|
||||
integer q
|
||||
integer g
|
||||
integer y
|
||||
|
||||
@rtype: C{str}
|
||||
"""
|
||||
type = self.type()
|
||||
data = self.data()
|
||||
if type == 'RSA':
|
||||
return (common.NS('ssh-rsa') + common.MP(data['e']) +
|
||||
common.MP(data['n']))
|
||||
elif type == 'DSA':
|
||||
return (common.NS('ssh-dss') + common.MP(data['p']) +
|
||||
common.MP(data['q']) + common.MP(data['g']) +
|
||||
common.MP(data['y']))
|
||||
|
||||
|
||||
def privateBlob(self):
|
||||
"""
|
||||
Return the private key blob for this key. The blob is the
|
||||
over-the-wire format for private keys:
|
||||
|
||||
RSA keys::
|
||||
string 'ssh-rsa'
|
||||
integer n
|
||||
integer e
|
||||
integer d
|
||||
integer u
|
||||
integer p
|
||||
integer q
|
||||
|
||||
DSA keys::
|
||||
string 'ssh-dss'
|
||||
integer p
|
||||
integer q
|
||||
integer g
|
||||
integer y
|
||||
integer x
|
||||
"""
|
||||
type = self.type()
|
||||
data = self.data()
|
||||
if type == 'RSA':
|
||||
return (common.NS('ssh-rsa') + common.MP(data['n']) +
|
||||
common.MP(data['e']) + common.MP(data['d']) +
|
||||
common.MP(data['u']) + common.MP(data['p']) +
|
||||
common.MP(data['q']))
|
||||
elif type == 'DSA':
|
||||
return (common.NS('ssh-dss') + common.MP(data['p']) +
|
||||
common.MP(data['q']) + common.MP(data['g']) +
|
||||
common.MP(data['y']) + common.MP(data['x']))
|
||||
|
||||
|
||||
def toString(self, type, extra=None):
|
||||
"""
|
||||
Create a string representation of this key. If the key is a private
|
||||
key and you want the represenation of its public key, use
|
||||
C{key.public().toString()}. type maps to a _toString_* method.
|
||||
|
||||
@param type: The type of string to emit. Currently supported values
|
||||
are C{'OPENSSH'}, C{'LSH'}, and C{'AGENTV3'}.
|
||||
@type type: L{str}
|
||||
|
||||
@param extra: Any extra data supported by the selected format which
|
||||
is not part of the key itself. For public OpenSSH keys, this is
|
||||
a comment. For private OpenSSH keys, this is a passphrase to
|
||||
encrypt with.
|
||||
@type extra: L{str} or L{NoneType}
|
||||
|
||||
@rtype: L{str}
|
||||
"""
|
||||
method = getattr(self, '_toString_%s' % type.upper(), None)
|
||||
if method is None:
|
||||
raise BadKeyError('unknown type: %s' % type)
|
||||
if method.func_code.co_argcount == 2:
|
||||
return method(extra)
|
||||
else:
|
||||
return method()
|
||||
|
||||
|
||||
def _toString_OPENSSH(self, extra):
|
||||
"""
|
||||
Return a public or private OpenSSH string. See
|
||||
_fromString_PUBLIC_OPENSSH and _fromString_PRIVATE_OPENSSH for the
|
||||
string formats. If extra is present, it represents a comment for a
|
||||
public key, or a passphrase for a private key.
|
||||
|
||||
@param extra: Comment for a public key or passphrase for a
|
||||
private key
|
||||
@type extra: C{str}
|
||||
|
||||
@rtype: C{str}
|
||||
"""
|
||||
data = self.data()
|
||||
if self.isPublic():
|
||||
b64Data = base64.encodestring(self.blob()).replace('\n', '')
|
||||
if not extra:
|
||||
extra = ''
|
||||
return ('%s %s %s' % (self.sshType(), b64Data, extra)).strip()
|
||||
else:
|
||||
lines = ['-----BEGIN %s PRIVATE KEY-----' % self.type()]
|
||||
if self.type() == 'RSA':
|
||||
p, q = data['p'], data['q']
|
||||
objData = (0, data['n'], data['e'], data['d'], q, p,
|
||||
data['d'] % (q - 1), data['d'] % (p - 1),
|
||||
data['u'])
|
||||
else:
|
||||
objData = (0, data['p'], data['q'], data['g'], data['y'],
|
||||
data['x'])
|
||||
asn1Sequence = univ.Sequence()
|
||||
for index, value in itertools.izip(itertools.count(), objData):
|
||||
asn1Sequence.setComponentByPosition(index, univ.Integer(value))
|
||||
asn1Data = berEncoder.encode(asn1Sequence)
|
||||
if extra:
|
||||
iv = randbytes.secureRandom(8)
|
||||
hexiv = ''.join(['%02X' % ord(x) for x in iv])
|
||||
lines.append('Proc-Type: 4,ENCRYPTED')
|
||||
lines.append('DEK-Info: DES-EDE3-CBC,%s\n' % hexiv)
|
||||
ba = md5(extra + iv).digest()
|
||||
bb = md5(ba + extra + iv).digest()
|
||||
encKey = (ba + bb)[:24]
|
||||
padLen = 8 - (len(asn1Data) % 8)
|
||||
asn1Data += (chr(padLen) * padLen)
|
||||
asn1Data = DES3.new(encKey, DES3.MODE_CBC,
|
||||
iv).encrypt(asn1Data)
|
||||
b64Data = base64.encodestring(asn1Data).replace('\n', '')
|
||||
lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)]
|
||||
lines.append('-----END %s PRIVATE KEY-----' % self.type())
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _toString_LSH(self):
|
||||
"""
|
||||
Return a public or private LSH key. See _fromString_PUBLIC_LSH and
|
||||
_fromString_PRIVATE_LSH for the key formats.
|
||||
|
||||
@rtype: C{str}
|
||||
"""
|
||||
data = self.data()
|
||||
if self.isPublic():
|
||||
if self.type() == 'RSA':
|
||||
keyData = sexpy.pack([['public-key',
|
||||
['rsa-pkcs1-sha1',
|
||||
['n', common.MP(data['n'])[4:]],
|
||||
['e', common.MP(data['e'])[4:]]]]])
|
||||
elif self.type() == 'DSA':
|
||||
keyData = sexpy.pack([['public-key',
|
||||
['dsa',
|
||||
['p', common.MP(data['p'])[4:]],
|
||||
['q', common.MP(data['q'])[4:]],
|
||||
['g', common.MP(data['g'])[4:]],
|
||||
['y', common.MP(data['y'])[4:]]]]])
|
||||
return '{' + base64.encodestring(keyData).replace('\n', '') + '}'
|
||||
else:
|
||||
if self.type() == 'RSA':
|
||||
p, q = data['p'], data['q']
|
||||
return sexpy.pack([['private-key',
|
||||
['rsa-pkcs1',
|
||||
['n', common.MP(data['n'])[4:]],
|
||||
['e', common.MP(data['e'])[4:]],
|
||||
['d', common.MP(data['d'])[4:]],
|
||||
['p', common.MP(q)[4:]],
|
||||
['q', common.MP(p)[4:]],
|
||||
['a', common.MP(data['d'] % (q - 1))[4:]],
|
||||
['b', common.MP(data['d'] % (p - 1))[4:]],
|
||||
['c', common.MP(data['u'])[4:]]]]])
|
||||
elif self.type() == 'DSA':
|
||||
return sexpy.pack([['private-key',
|
||||
['dsa',
|
||||
['p', common.MP(data['p'])[4:]],
|
||||
['q', common.MP(data['q'])[4:]],
|
||||
['g', common.MP(data['g'])[4:]],
|
||||
['y', common.MP(data['y'])[4:]],
|
||||
['x', common.MP(data['x'])[4:]]]]])
|
||||
|
||||
|
||||
def _toString_AGENTV3(self):
|
||||
"""
|
||||
Return a private Secure Shell Agent v3 key. See
|
||||
_fromString_AGENTV3 for the key format.
|
||||
|
||||
@rtype: C{str}
|
||||
"""
|
||||
data = self.data()
|
||||
if not self.isPublic():
|
||||
if self.type() == 'RSA':
|
||||
values = (data['e'], data['d'], data['n'], data['u'],
|
||||
data['p'], data['q'])
|
||||
elif self.type() == 'DSA':
|
||||
values = (data['p'], data['q'], data['g'], data['y'],
|
||||
data['x'])
|
||||
return common.NS(self.sshType()) + ''.join(map(common.MP, values))
|
||||
|
||||
|
||||
def sign(self, data):
|
||||
"""
|
||||
Returns a signature with this Key.
|
||||
|
||||
@type data: C{str}
|
||||
@rtype: C{str}
|
||||
"""
|
||||
if self.type() == 'RSA':
|
||||
digest = pkcs1Digest(data, self.keyObject.size() / 8)
|
||||
signature = self.keyObject.sign(digest, '')[0]
|
||||
ret = common.NS(Util.number.long_to_bytes(signature))
|
||||
elif self.type() == 'DSA':
|
||||
digest = sha1(data).digest()
|
||||
randomBytes = randbytes.secureRandom(19)
|
||||
sig = self.keyObject.sign(digest, randomBytes)
|
||||
# SSH insists that the DSS signature blob be two 160-bit integers
|
||||
# concatenated together. The sig[0], [1] numbers from obj.sign
|
||||
# are just numbers, and could be any length from 0 to 160 bits.
|
||||
# Make sure they are padded out to 160 bits (20 bytes each)
|
||||
ret = common.NS(Util.number.long_to_bytes(sig[0], 20) +
|
||||
Util.number.long_to_bytes(sig[1], 20))
|
||||
return common.NS(self.sshType()) + ret
|
||||
|
||||
|
||||
def verify(self, signature, data):
|
||||
"""
|
||||
Returns true if the signature for data is valid for this Key.
|
||||
|
||||
@type signature: C{str}
|
||||
@type data: C{str}
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
if len(signature) == 40:
|
||||
# DSA key with no padding
|
||||
signatureType, signature = 'ssh-dss', common.NS(signature)
|
||||
else:
|
||||
signatureType, signature = common.getNS(signature)
|
||||
if signatureType != self.sshType():
|
||||
return False
|
||||
if self.type() == 'RSA':
|
||||
numbers = common.getMP(signature)
|
||||
digest = pkcs1Digest(data, self.keyObject.size() / 8)
|
||||
elif self.type() == 'DSA':
|
||||
signature = common.getNS(signature)[0]
|
||||
numbers = [Util.number.bytes_to_long(n) for n in signature[:20],
|
||||
signature[20:]]
|
||||
digest = sha1(data).digest()
|
||||
return self.keyObject.verify(digest, numbers)
|
||||
|
||||
|
||||
|
||||
def objectType(obj):
|
||||
"""
|
||||
Return the SSH key type corresponding to a
|
||||
C{Crypto.PublicKey.pubkey.pubkey} object.
|
||||
|
||||
@type obj: C{Crypto.PublicKey.pubkey.pubkey}
|
||||
@rtype: C{str}
|
||||
"""
|
||||
keyDataMapping = {
|
||||
('n', 'e', 'd', 'p', 'q'): 'ssh-rsa',
|
||||
('n', 'e', 'd', 'p', 'q', 'u'): 'ssh-rsa',
|
||||
('y', 'g', 'p', 'q', 'x'): 'ssh-dss'
|
||||
}
|
||||
try:
|
||||
return keyDataMapping[tuple(obj.keydata)]
|
||||
except (KeyError, AttributeError):
|
||||
raise BadKeyError("invalid key object", obj)
|
||||
|
||||
|
||||
|
||||
def pkcs1Pad(data, messageLength):
|
||||
"""
|
||||
Pad out data to messageLength according to the PKCS#1 standard.
|
||||
@type data: C{str}
|
||||
@type messageLength: C{int}
|
||||
"""
|
||||
lenPad = messageLength - 2 - len(data)
|
||||
return '\x01' + ('\xff' * lenPad) + '\x00' + data
|
||||
|
||||
|
||||
|
||||
def pkcs1Digest(data, messageLength):
|
||||
"""
|
||||
Create a message digest using the SHA1 hash algorithm according to the
|
||||
PKCS#1 standard.
|
||||
@type data: C{str}
|
||||
@type messageLength: C{str}
|
||||
"""
|
||||
digest = sha1(data).digest()
|
||||
return pkcs1Pad(ID_SHA1 + digest, messageLength)
|
||||
|
||||
|
||||
|
||||
def lenSig(obj):
|
||||
"""
|
||||
Return the length of the signature in bytes for a key object.
|
||||
|
||||
@type obj: C{Crypto.PublicKey.pubkey.pubkey}
|
||||
@rtype: C{long}
|
||||
"""
|
||||
return obj.size() / 8
|
||||
|
||||
|
||||
ID_SHA1 = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The parent class for all the SSH services. Currently implemented services
|
||||
are ssh-userauth and ssh-connection.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
from twisted.python import log
|
||||
|
||||
class SSHService(log.Logger):
|
||||
name = None # this is the ssh name for the service
|
||||
protocolMessages = {} # these map #'s -> protocol names
|
||||
transport = None # gets set later
|
||||
|
||||
def serviceStarted(self):
|
||||
"""
|
||||
called when the service is active on the transport.
|
||||
"""
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
called when the service is stopped, either by the connection ending
|
||||
or by another service being started
|
||||
"""
|
||||
|
||||
def logPrefix(self):
|
||||
return "SSHService %s on %s" % (self.name,
|
||||
self.transport.transport.logPrefix())
|
||||
|
||||
def packetReceived(self, messageNum, packet):
|
||||
"""
|
||||
called when we receive a packet on the transport
|
||||
"""
|
||||
#print self.protocolMessages
|
||||
if messageNum in self.protocolMessages:
|
||||
messageType = self.protocolMessages[messageNum]
|
||||
f = getattr(self,'ssh_%s' % messageType[4:],
|
||||
None)
|
||||
if f is not None:
|
||||
return f(packet)
|
||||
log.msg("couldn't handle %r" % messageNum)
|
||||
log.msg(repr(packet))
|
||||
self.transport.sendUnimplemented()
|
||||
|
||||
348
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/session.py
Normal file
348
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/session.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_session -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of SSHSession, which (by default)
|
||||
allows access to a shell and a python interpreter over SSH.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import struct
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
from zope.interface import implements
|
||||
|
||||
from twisted.internet import interfaces, protocol
|
||||
from twisted.python import log
|
||||
from twisted.conch.interfaces import ISession
|
||||
from twisted.conch.ssh import common, channel
|
||||
|
||||
class SSHSession(channel.SSHChannel):
|
||||
|
||||
name = 'session'
|
||||
def __init__(self, *args, **kw):
|
||||
channel.SSHChannel.__init__(self, *args, **kw)
|
||||
self.buf = ''
|
||||
self.client = None
|
||||
self.session = None
|
||||
|
||||
def request_subsystem(self, data):
|
||||
subsystem, ignored= common.getNS(data)
|
||||
log.msg('asking for subsystem "%s"' % subsystem)
|
||||
client = self.avatar.lookupSubsystem(subsystem, data)
|
||||
if client:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
proto = wrapProcessProtocol(pp)
|
||||
client.makeConnection(proto)
|
||||
pp.makeConnection(wrapProtocol(client))
|
||||
self.client = pp
|
||||
return 1
|
||||
else:
|
||||
log.msg('failed to get subsystem')
|
||||
return 0
|
||||
|
||||
def request_shell(self, data):
|
||||
log.msg('getting shell')
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
try:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
self.session.openShell(pp)
|
||||
except:
|
||||
log.deferr()
|
||||
return 0
|
||||
else:
|
||||
self.client = pp
|
||||
return 1
|
||||
|
||||
def request_exec(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
f,data = common.getNS(data)
|
||||
log.msg('executing command "%s"' % f)
|
||||
try:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
self.session.execCommand(pp, f)
|
||||
except:
|
||||
log.deferr()
|
||||
return 0
|
||||
else:
|
||||
self.client = pp
|
||||
return 1
|
||||
|
||||
def request_pty_req(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
term, windowSize, modes = parseRequest_pty_req(data)
|
||||
log.msg('pty request: %s %s' % (term, windowSize))
|
||||
try:
|
||||
self.session.getPty(term, windowSize, modes)
|
||||
except:
|
||||
log.err()
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def request_window_change(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
winSize = parseRequest_window_change(data)
|
||||
try:
|
||||
self.session.windowChanged(winSize)
|
||||
except:
|
||||
log.msg('error changing window size')
|
||||
log.err()
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def dataReceived(self, data):
|
||||
if not self.client:
|
||||
#self.conn.sendClose(self)
|
||||
self.buf += data
|
||||
return
|
||||
self.client.transport.write(data)
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
if dataType == connection.EXTENDED_DATA_STDERR:
|
||||
if self.client and hasattr(self.client.transport, 'writeErr'):
|
||||
self.client.transport.writeErr(data)
|
||||
else:
|
||||
log.msg('weird extended data: %s'%dataType)
|
||||
|
||||
def eofReceived(self):
|
||||
if self.session:
|
||||
self.session.eofReceived()
|
||||
elif self.client:
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def closed(self):
|
||||
if self.session:
|
||||
self.session.closed()
|
||||
elif self.client:
|
||||
self.client.transport.loseConnection()
|
||||
|
||||
#def closeReceived(self):
|
||||
# self.loseConnection() # don't know what to do with this
|
||||
|
||||
def loseConnection(self):
|
||||
if self.client:
|
||||
self.client.transport.loseConnection()
|
||||
channel.SSHChannel.loseConnection(self)
|
||||
|
||||
class _ProtocolWrapper(protocol.ProcessProtocol):
|
||||
"""
|
||||
This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
|
||||
"""
|
||||
def __init__(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def connectionMade(self): self.proto.connectionMade()
|
||||
|
||||
def outReceived(self, data): self.proto.dataReceived(data)
|
||||
|
||||
def processEnded(self, reason): self.proto.connectionLost(reason)
|
||||
|
||||
class _DummyTransport:
|
||||
|
||||
def __init__(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.proto.transport.write(data)
|
||||
|
||||
def write(self, data):
|
||||
self.proto.dataReceived(data)
|
||||
|
||||
def writeSequence(self, seq):
|
||||
self.write(''.join(seq))
|
||||
|
||||
def loseConnection(self):
|
||||
self.proto.connectionLost(protocol.connectionDone)
|
||||
|
||||
def wrapProcessProtocol(inst):
|
||||
if isinstance(inst, protocol.Protocol):
|
||||
return _ProtocolWrapper(inst)
|
||||
else:
|
||||
return inst
|
||||
|
||||
def wrapProtocol(proto):
|
||||
return _DummyTransport(proto)
|
||||
|
||||
|
||||
|
||||
# SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
|
||||
# to accept. See RFC 4254
|
||||
SUPPORTED_SIGNALS = ["ABRT", "ALRM", "FPE", "HUP", "ILL", "INT", "KILL",
|
||||
"PIPE", "QUIT", "SEGV", "TERM", "USR1", "USR2"]
|
||||
|
||||
|
||||
|
||||
class SSHSessionProcessProtocol(protocol.ProcessProtocol):
|
||||
"""I am both an L{IProcessProtocol} and an L{ITransport}.
|
||||
|
||||
I am a transport to the remote endpoint and a process protocol to the
|
||||
local subsystem.
|
||||
"""
|
||||
|
||||
implements(interfaces.ITransport)
|
||||
|
||||
# once initialized, a dictionary mapping signal values to strings
|
||||
# that follow RFC 4254.
|
||||
_signalValuesToNames = None
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.lostOutOrErrFlag = False
|
||||
|
||||
def connectionMade(self):
|
||||
if self.session.buf:
|
||||
self.transport.write(self.session.buf)
|
||||
self.session.buf = None
|
||||
|
||||
def outReceived(self, data):
|
||||
self.session.write(data)
|
||||
|
||||
def errReceived(self, err):
|
||||
self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
|
||||
|
||||
def outConnectionLost(self):
|
||||
"""
|
||||
EOF should only be sent when both STDOUT and STDERR have been closed.
|
||||
"""
|
||||
if self.lostOutOrErrFlag:
|
||||
self.session.conn.sendEOF(self.session)
|
||||
else:
|
||||
self.lostOutOrErrFlag = True
|
||||
|
||||
def errConnectionLost(self):
|
||||
"""
|
||||
See outConnectionLost().
|
||||
"""
|
||||
self.outConnectionLost()
|
||||
|
||||
def connectionLost(self, reason = None):
|
||||
self.session.loseConnection()
|
||||
|
||||
|
||||
def _getSignalName(self, signum):
|
||||
"""
|
||||
Get a signal name given a signal number.
|
||||
"""
|
||||
if self._signalValuesToNames is None:
|
||||
self._signalValuesToNames = {}
|
||||
# make sure that the POSIX ones are the defaults
|
||||
for signame in SUPPORTED_SIGNALS:
|
||||
signame = 'SIG' + signame
|
||||
sigvalue = getattr(signal, signame, None)
|
||||
if sigvalue is not None:
|
||||
self._signalValuesToNames[sigvalue] = signame
|
||||
for k, v in signal.__dict__.items():
|
||||
# Check for platform specific signals, ignoring Python specific
|
||||
# SIG_DFL and SIG_IGN
|
||||
if k.startswith('SIG') and not k.startswith('SIG_'):
|
||||
if v not in self._signalValuesToNames:
|
||||
self._signalValuesToNames[v] = k + '@' + sys.platform
|
||||
return self._signalValuesToNames[signum]
|
||||
|
||||
|
||||
def processEnded(self, reason=None):
|
||||
"""
|
||||
When we are told the process ended, try to notify the other side about
|
||||
how the process ended using the exit-signal or exit-status requests.
|
||||
Also, close the channel.
|
||||
"""
|
||||
if reason is not None:
|
||||
err = reason.value
|
||||
if err.signal is not None:
|
||||
signame = self._getSignalName(err.signal)
|
||||
if (getattr(os, 'WCOREDUMP', None) is not None and
|
||||
os.WCOREDUMP(err.status)):
|
||||
log.msg('exitSignal: %s (core dumped)' % (signame,))
|
||||
coreDumped = 1
|
||||
else:
|
||||
log.msg('exitSignal: %s' % (signame,))
|
||||
coreDumped = 0
|
||||
self.session.conn.sendRequest(self.session, 'exit-signal',
|
||||
common.NS(signame[3:]) + chr(coreDumped) +
|
||||
common.NS('') + common.NS(''))
|
||||
elif err.exitCode is not None:
|
||||
log.msg('exitCode: %r' % (err.exitCode,))
|
||||
self.session.conn.sendRequest(self.session, 'exit-status',
|
||||
struct.pack('>L', err.exitCode))
|
||||
self.session.loseConnection()
|
||||
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
Return the host from my session's transport.
|
||||
"""
|
||||
return self.session.conn.transport.getHost()
|
||||
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
Return the peer from my session's transport.
|
||||
"""
|
||||
return self.session.conn.transport.getPeer()
|
||||
|
||||
|
||||
def write(self, data):
|
||||
self.session.write(data)
|
||||
|
||||
|
||||
def writeSequence(self, seq):
|
||||
self.session.write(''.join(seq))
|
||||
|
||||
|
||||
def loseConnection(self):
|
||||
self.session.loseConnection()
|
||||
|
||||
|
||||
|
||||
class SSHSessionClient(protocol.Protocol):
|
||||
|
||||
def dataReceived(self, data):
|
||||
if self.transport:
|
||||
self.transport.write(data)
|
||||
|
||||
# methods factored out to make live easier on server writers
|
||||
def parseRequest_pty_req(data):
|
||||
"""Parse the data from a pty-req request into usable data.
|
||||
|
||||
@returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
|
||||
"""
|
||||
term, rest = common.getNS(data)
|
||||
cols, rows, xpixel, ypixel = struct.unpack('>4L', rest[: 16])
|
||||
modes, ignored= common.getNS(rest[16:])
|
||||
winSize = (rows, cols, xpixel, ypixel)
|
||||
modes = [(ord(modes[i]), struct.unpack('>L', modes[i+1: i+5])[0]) for i in range(0, len(modes)-1, 5)]
|
||||
return term, winSize, modes
|
||||
|
||||
def packRequest_pty_req(term, (rows, cols, xpixel, ypixel), modes):
|
||||
"""Pack a pty-req request so that it is suitable for sending.
|
||||
|
||||
NOTE: modes must be packed before being sent here.
|
||||
"""
|
||||
termPacked = common.NS(term)
|
||||
winSizePacked = struct.pack('>4L', cols, rows, xpixel, ypixel)
|
||||
modesPacked = common.NS(modes) # depend on the client packing modes
|
||||
return termPacked + winSizePacked + modesPacked
|
||||
|
||||
def parseRequest_window_change(data):
|
||||
"""Parse the data from a window-change request into usuable data.
|
||||
|
||||
@returns: a tuple of (rows, cols, xpixel, ypixel)
|
||||
"""
|
||||
cols, rows, xpixel, ypixel = struct.unpack('>4L', data)
|
||||
return rows, cols, xpixel, ypixel
|
||||
|
||||
def packRequest_window_change((rows, cols, xpixel, ypixel)):
|
||||
"""Pack a window-change request so that it is suitable for sending.
|
||||
"""
|
||||
return struct.pack('>4L', cols, rows, xpixel, ypixel)
|
||||
|
||||
import connection
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
def parse(s):
|
||||
s = s.strip()
|
||||
expr = []
|
||||
while s:
|
||||
if s[0] == '(':
|
||||
newSexp = []
|
||||
if expr:
|
||||
expr[-1].append(newSexp)
|
||||
expr.append(newSexp)
|
||||
s = s[1:]
|
||||
continue
|
||||
if s[0] == ')':
|
||||
aList = expr.pop()
|
||||
s=s[1:]
|
||||
if not expr:
|
||||
assert not s
|
||||
return aList
|
||||
continue
|
||||
i = 0
|
||||
while s[i].isdigit(): i+=1
|
||||
assert i
|
||||
length = int(s[:i])
|
||||
data = s[i+1:i+1+length]
|
||||
expr[-1].append(data)
|
||||
s=s[i+1+length:]
|
||||
assert 0, "this should not happen"
|
||||
|
||||
def pack(sexp):
|
||||
s = ""
|
||||
for o in sexp:
|
||||
if type(o) in (type(()), type([])):
|
||||
s+='('
|
||||
s+=pack(o)
|
||||
s+=')'
|
||||
else:
|
||||
s+='%i:%s' % (len(o), o)
|
||||
return s
|
||||
1639
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/transport.py
Normal file
1639
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/transport.py
Normal file
File diff suppressed because it is too large
Load diff
838
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/userauth.py
Normal file
838
Darwin/lib/python2.7/site-packages/twisted/conch/ssh/userauth.py
Normal file
|
|
@ -0,0 +1,838 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_userauth -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation of the ssh-userauth service.
|
||||
Currently implemented authentication types are public-key and password.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import struct
|
||||
from twisted.conch import error, interfaces
|
||||
from twisted.conch.ssh import keys, transport, service
|
||||
from twisted.conch.ssh.common import NS, getNS
|
||||
from twisted.cred import credentials
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.python import failure, log
|
||||
|
||||
|
||||
|
||||
class SSHUserAuthServer(service.SSHService):
|
||||
"""
|
||||
A service implementing the server side of the 'ssh-userauth' service. It
|
||||
is used to authenticate the user on the other side as being able to access
|
||||
this server.
|
||||
|
||||
@ivar name: the name of this service: 'ssh-userauth'
|
||||
@type name: C{str}
|
||||
@ivar authenticatedWith: a list of authentication methods that have
|
||||
already been used.
|
||||
@type authenticatedWith: C{list}
|
||||
@ivar loginTimeout: the number of seconds we wait before disconnecting
|
||||
the user for taking too long to authenticate
|
||||
@type loginTimeout: C{int}
|
||||
@ivar attemptsBeforeDisconnect: the number of failed login attempts we
|
||||
allow before disconnecting.
|
||||
@type attemptsBeforeDisconnect: C{int}
|
||||
@ivar loginAttempts: the number of login attempts that have been made
|
||||
@type loginAttempts: C{int}
|
||||
@ivar passwordDelay: the number of seconds to delay when the user gives
|
||||
an incorrect password
|
||||
@type passwordDelay: C{int}
|
||||
@ivar interfaceToMethod: a C{dict} mapping credential interfaces to
|
||||
authentication methods. The server checks to see which of the
|
||||
cred interfaces have checkers and tells the client that those methods
|
||||
are valid for authentication.
|
||||
@type interfaceToMethod: C{dict}
|
||||
@ivar supportedAuthentications: A list of the supported authentication
|
||||
methods.
|
||||
@type supportedAuthentications: C{list} of C{str}
|
||||
@ivar user: the last username the client tried to authenticate with
|
||||
@type user: C{str}
|
||||
@ivar method: the current authentication method
|
||||
@type method: C{str}
|
||||
@ivar nextService: the service the user wants started after authentication
|
||||
has been completed.
|
||||
@type nextService: C{str}
|
||||
@ivar portal: the L{twisted.cred.portal.Portal} we are using for
|
||||
authentication
|
||||
@type portal: L{twisted.cred.portal.Portal}
|
||||
@ivar clock: an object with a callLater method. Stubbed out for testing.
|
||||
"""
|
||||
|
||||
|
||||
name = 'ssh-userauth'
|
||||
loginTimeout = 10 * 60 * 60
|
||||
# 10 minutes before we disconnect them
|
||||
attemptsBeforeDisconnect = 20
|
||||
# 20 login attempts before a disconnect
|
||||
passwordDelay = 1 # number of seconds to delay on a failed password
|
||||
clock = reactor
|
||||
interfaceToMethod = {
|
||||
credentials.ISSHPrivateKey : 'publickey',
|
||||
credentials.IUsernamePassword : 'password',
|
||||
credentials.IPluggableAuthenticationModules : 'keyboard-interactive',
|
||||
}
|
||||
|
||||
|
||||
def serviceStarted(self):
|
||||
"""
|
||||
Called when the userauth service is started. Set up instance
|
||||
variables, check if we should allow password/keyboard-interactive
|
||||
authentication (only allow if the outgoing connection is encrypted) and
|
||||
set up a login timeout.
|
||||
"""
|
||||
self.authenticatedWith = []
|
||||
self.loginAttempts = 0
|
||||
self.user = None
|
||||
self.nextService = None
|
||||
self._pamDeferred = None
|
||||
self.portal = self.transport.factory.portal
|
||||
|
||||
self.supportedAuthentications = []
|
||||
for i in self.portal.listCredentialsInterfaces():
|
||||
if i in self.interfaceToMethod:
|
||||
self.supportedAuthentications.append(self.interfaceToMethod[i])
|
||||
|
||||
if not self.transport.isEncrypted('in'):
|
||||
# don't let us transport password in plaintext
|
||||
if 'password' in self.supportedAuthentications:
|
||||
self.supportedAuthentications.remove('password')
|
||||
if 'keyboard-interactive' in self.supportedAuthentications:
|
||||
self.supportedAuthentications.remove('keyboard-interactive')
|
||||
self._cancelLoginTimeout = self.clock.callLater(
|
||||
self.loginTimeout,
|
||||
self.timeoutAuthentication)
|
||||
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
Called when the userauth service is stopped. Cancel the login timeout
|
||||
if it's still going.
|
||||
"""
|
||||
if self._cancelLoginTimeout:
|
||||
self._cancelLoginTimeout.cancel()
|
||||
self._cancelLoginTimeout = None
|
||||
|
||||
|
||||
def timeoutAuthentication(self):
|
||||
"""
|
||||
Called when the user has timed out on authentication. Disconnect
|
||||
with a DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE message.
|
||||
"""
|
||||
self._cancelLoginTimeout = None
|
||||
self.transport.sendDisconnect(
|
||||
transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
|
||||
'you took too long')
|
||||
|
||||
|
||||
def tryAuth(self, kind, user, data):
|
||||
"""
|
||||
Try to authenticate the user with the given method. Dispatches to a
|
||||
auth_* method.
|
||||
|
||||
@param kind: the authentication method to try.
|
||||
@type kind: C{str}
|
||||
@param user: the username the client is authenticating with.
|
||||
@type user: C{str}
|
||||
@param data: authentication specific data sent by the client.
|
||||
@type data: C{str}
|
||||
@return: A Deferred called back if the method succeeded, or erred back
|
||||
if it failed.
|
||||
@rtype: C{defer.Deferred}
|
||||
"""
|
||||
log.msg('%s trying auth %s' % (user, kind))
|
||||
if kind not in self.supportedAuthentications:
|
||||
return defer.fail(
|
||||
error.ConchError('unsupported authentication, failing'))
|
||||
kind = kind.replace('-', '_')
|
||||
f = getattr(self,'auth_%s'%kind, None)
|
||||
if f:
|
||||
ret = f(data)
|
||||
if not ret:
|
||||
return defer.fail(
|
||||
error.ConchError('%s return None instead of a Deferred'
|
||||
% kind))
|
||||
else:
|
||||
return ret
|
||||
return defer.fail(error.ConchError('bad auth type: %s' % kind))
|
||||
|
||||
|
||||
def ssh_USERAUTH_REQUEST(self, packet):
|
||||
"""
|
||||
The client has requested authentication. Payload::
|
||||
string user
|
||||
string next service
|
||||
string method
|
||||
<authentication specific data>
|
||||
|
||||
@type packet: C{str}
|
||||
"""
|
||||
user, nextService, method, rest = getNS(packet, 3)
|
||||
if user != self.user or nextService != self.nextService:
|
||||
self.authenticatedWith = [] # clear auth state
|
||||
self.user = user
|
||||
self.nextService = nextService
|
||||
self.method = method
|
||||
d = self.tryAuth(method, user, rest)
|
||||
if not d:
|
||||
self._ebBadAuth(
|
||||
failure.Failure(error.ConchError('auth returned none')))
|
||||
return
|
||||
d.addCallback(self._cbFinishedAuth)
|
||||
d.addErrback(self._ebMaybeBadAuth)
|
||||
d.addErrback(self._ebBadAuth)
|
||||
return d
|
||||
|
||||
|
||||
def _cbFinishedAuth(self, (interface, avatar, logout)):
|
||||
"""
|
||||
The callback when user has successfully been authenticated. For a
|
||||
description of the arguments, see L{twisted.cred.portal.Portal.login}.
|
||||
We start the service requested by the user.
|
||||
"""
|
||||
self.transport.avatar = avatar
|
||||
self.transport.logoutFunction = logout
|
||||
service = self.transport.factory.getService(self.transport,
|
||||
self.nextService)
|
||||
if not service:
|
||||
raise error.ConchError('could not get next service: %s'
|
||||
% self.nextService)
|
||||
log.msg('%s authenticated with %s' % (self.user, self.method))
|
||||
self.transport.sendPacket(MSG_USERAUTH_SUCCESS, '')
|
||||
self.transport.setService(service())
|
||||
|
||||
|
||||
def _ebMaybeBadAuth(self, reason):
|
||||
"""
|
||||
An intermediate errback. If the reason is
|
||||
error.NotEnoughAuthentication, we send a MSG_USERAUTH_FAILURE, but
|
||||
with the partial success indicator set.
|
||||
|
||||
@type reason: L{twisted.python.failure.Failure}
|
||||
"""
|
||||
reason.trap(error.NotEnoughAuthentication)
|
||||
self.transport.sendPacket(MSG_USERAUTH_FAILURE,
|
||||
NS(','.join(self.supportedAuthentications)) + '\xff')
|
||||
|
||||
|
||||
def _ebBadAuth(self, reason):
|
||||
"""
|
||||
The final errback in the authentication chain. If the reason is
|
||||
error.IgnoreAuthentication, we simply return; the authentication
|
||||
method has sent its own response. Otherwise, send a failure message
|
||||
and (if the method is not 'none') increment the number of login
|
||||
attempts.
|
||||
|
||||
@type reason: L{twisted.python.failure.Failure}
|
||||
"""
|
||||
if reason.check(error.IgnoreAuthentication):
|
||||
return
|
||||
if self.method != 'none':
|
||||
log.msg('%s failed auth %s' % (self.user, self.method))
|
||||
if reason.check(UnauthorizedLogin):
|
||||
log.msg('unauthorized login: %s' % reason.getErrorMessage())
|
||||
elif reason.check(error.ConchError):
|
||||
log.msg('reason: %s' % reason.getErrorMessage())
|
||||
else:
|
||||
log.msg(reason.getTraceback())
|
||||
self.loginAttempts += 1
|
||||
if self.loginAttempts > self.attemptsBeforeDisconnect:
|
||||
self.transport.sendDisconnect(
|
||||
transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
|
||||
'too many bad auths')
|
||||
return
|
||||
self.transport.sendPacket(
|
||||
MSG_USERAUTH_FAILURE,
|
||||
NS(','.join(self.supportedAuthentications)) + '\x00')
|
||||
|
||||
|
||||
def auth_publickey(self, packet):
|
||||
"""
|
||||
Public key authentication. Payload::
|
||||
byte has signature
|
||||
string algorithm name
|
||||
string key blob
|
||||
[string signature] (if has signature is True)
|
||||
|
||||
Create a SSHPublicKey credential and verify it using our portal.
|
||||
"""
|
||||
hasSig = ord(packet[0])
|
||||
algName, blob, rest = getNS(packet[1:], 2)
|
||||
pubKey = keys.Key.fromString(blob)
|
||||
signature = hasSig and getNS(rest)[0] or None
|
||||
if hasSig:
|
||||
b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) +
|
||||
NS(self.user) + NS(self.nextService) + NS('publickey') +
|
||||
chr(hasSig) + NS(pubKey.sshType()) + NS(blob))
|
||||
c = credentials.SSHPrivateKey(self.user, algName, blob, b,
|
||||
signature)
|
||||
return self.portal.login(c, None, interfaces.IConchUser)
|
||||
else:
|
||||
c = credentials.SSHPrivateKey(self.user, algName, blob, None, None)
|
||||
return self.portal.login(c, None,
|
||||
interfaces.IConchUser).addErrback(self._ebCheckKey,
|
||||
packet[1:])
|
||||
|
||||
|
||||
def _ebCheckKey(self, reason, packet):
|
||||
"""
|
||||
Called back if the user did not sent a signature. If reason is
|
||||
error.ValidPublicKey then this key is valid for the user to
|
||||
authenticate with. Send MSG_USERAUTH_PK_OK.
|
||||
"""
|
||||
reason.trap(error.ValidPublicKey)
|
||||
# if we make it here, it means that the publickey is valid
|
||||
self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet)
|
||||
return failure.Failure(error.IgnoreAuthentication())
|
||||
|
||||
|
||||
def auth_password(self, packet):
|
||||
"""
|
||||
Password authentication. Payload::
|
||||
string password
|
||||
|
||||
Make a UsernamePassword credential and verify it with our portal.
|
||||
"""
|
||||
password = getNS(packet[1:])[0]
|
||||
c = credentials.UsernamePassword(self.user, password)
|
||||
return self.portal.login(c, None, interfaces.IConchUser).addErrback(
|
||||
self._ebPassword)
|
||||
|
||||
|
||||
def _ebPassword(self, f):
|
||||
"""
|
||||
If the password is invalid, wait before sending the failure in order
|
||||
to delay brute-force password guessing.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
self.clock.callLater(self.passwordDelay, d.callback, f)
|
||||
return d
|
||||
|
||||
|
||||
def auth_keyboard_interactive(self, packet):
|
||||
"""
|
||||
Keyboard interactive authentication. No payload. We create a
|
||||
PluggableAuthenticationModules credential and authenticate with our
|
||||
portal.
|
||||
"""
|
||||
if self._pamDeferred is not None:
|
||||
self.transport.sendDisconnect(
|
||||
transport.DISCONNECT_PROTOCOL_ERROR,
|
||||
"only one keyboard interactive attempt at a time")
|
||||
return defer.fail(error.IgnoreAuthentication())
|
||||
c = credentials.PluggableAuthenticationModules(self.user,
|
||||
self._pamConv)
|
||||
return self.portal.login(c, None, interfaces.IConchUser)
|
||||
|
||||
|
||||
def _pamConv(self, items):
|
||||
"""
|
||||
Convert a list of PAM authentication questions into a
|
||||
MSG_USERAUTH_INFO_REQUEST. Returns a Deferred that will be called
|
||||
back when the user has responses to the questions.
|
||||
|
||||
@param items: a list of 2-tuples (message, kind). We only care about
|
||||
kinds 1 (password) and 2 (text).
|
||||
@type items: C{list}
|
||||
@rtype: L{defer.Deferred}
|
||||
"""
|
||||
resp = []
|
||||
for message, kind in items:
|
||||
if kind == 1: # password
|
||||
resp.append((message, 0))
|
||||
elif kind == 2: # text
|
||||
resp.append((message, 1))
|
||||
elif kind in (3, 4):
|
||||
return defer.fail(error.ConchError(
|
||||
'cannot handle PAM 3 or 4 messages'))
|
||||
else:
|
||||
return defer.fail(error.ConchError(
|
||||
'bad PAM auth kind %i' % kind))
|
||||
packet = NS('') + NS('') + NS('')
|
||||
packet += struct.pack('>L', len(resp))
|
||||
for prompt, echo in resp:
|
||||
packet += NS(prompt)
|
||||
packet += chr(echo)
|
||||
self.transport.sendPacket(MSG_USERAUTH_INFO_REQUEST, packet)
|
||||
self._pamDeferred = defer.Deferred()
|
||||
return self._pamDeferred
|
||||
|
||||
|
||||
def ssh_USERAUTH_INFO_RESPONSE(self, packet):
|
||||
"""
|
||||
The user has responded with answers to PAMs authentication questions.
|
||||
Parse the packet into a PAM response and callback self._pamDeferred.
|
||||
Payload::
|
||||
uint32 numer of responses
|
||||
string response 1
|
||||
...
|
||||
string response n
|
||||
"""
|
||||
d, self._pamDeferred = self._pamDeferred, None
|
||||
|
||||
try:
|
||||
resp = []
|
||||
numResps = struct.unpack('>L', packet[:4])[0]
|
||||
packet = packet[4:]
|
||||
while len(resp) < numResps:
|
||||
response, packet = getNS(packet)
|
||||
resp.append((response, 0))
|
||||
if packet:
|
||||
raise error.ConchError("%i bytes of extra data" % len(packet))
|
||||
except:
|
||||
d.errback(failure.Failure())
|
||||
else:
|
||||
d.callback(resp)
|
||||
|
||||
|
||||
|
||||
class SSHUserAuthClient(service.SSHService):
|
||||
"""
|
||||
A service implementing the client side of 'ssh-userauth'.
|
||||
|
||||
This service will try all authentication methods provided by the server,
|
||||
making callbacks for more information when necessary.
|
||||
|
||||
@ivar name: the name of this service: 'ssh-userauth'
|
||||
@type name: C{str}
|
||||
@ivar preferredOrder: a list of authentication methods that should be used
|
||||
first, in order of preference, if supported by the server
|
||||
@type preferredOrder: C{list}
|
||||
@ivar user: the name of the user to authenticate as
|
||||
@type user: C{str}
|
||||
@ivar instance: the service to start after authentication has finished
|
||||
@type instance: L{service.SSHService}
|
||||
@ivar authenticatedWith: a list of strings of authentication methods we've tried
|
||||
@type authenticatedWith: C{list} of C{str}
|
||||
@ivar triedPublicKeys: a list of public key objects that we've tried to
|
||||
authenticate with
|
||||
@type triedPublicKeys: C{list} of L{Key}
|
||||
@ivar lastPublicKey: the last public key object we've tried to authenticate
|
||||
with
|
||||
@type lastPublicKey: L{Key}
|
||||
"""
|
||||
|
||||
|
||||
name = 'ssh-userauth'
|
||||
preferredOrder = ['publickey', 'password', 'keyboard-interactive']
|
||||
|
||||
|
||||
def __init__(self, user, instance):
|
||||
self.user = user
|
||||
self.instance = instance
|
||||
|
||||
|
||||
def serviceStarted(self):
|
||||
self.authenticatedWith = []
|
||||
self.triedPublicKeys = []
|
||||
self.lastPublicKey = None
|
||||
self.askForAuth('none', '')
|
||||
|
||||
|
||||
def askForAuth(self, kind, extraData):
|
||||
"""
|
||||
Send a MSG_USERAUTH_REQUEST.
|
||||
|
||||
@param kind: the authentication method to try.
|
||||
@type kind: C{str}
|
||||
@param extraData: method-specific data to go in the packet
|
||||
@type extraData: C{str}
|
||||
"""
|
||||
self.lastAuth = kind
|
||||
self.transport.sendPacket(MSG_USERAUTH_REQUEST, NS(self.user) +
|
||||
NS(self.instance.name) + NS(kind) + extraData)
|
||||
|
||||
|
||||
def tryAuth(self, kind):
|
||||
"""
|
||||
Dispatch to an authentication method.
|
||||
|
||||
@param kind: the authentication method
|
||||
@type kind: C{str}
|
||||
"""
|
||||
kind = kind.replace('-', '_')
|
||||
log.msg('trying to auth with %s' % (kind,))
|
||||
f = getattr(self,'auth_%s' % (kind,), None)
|
||||
if f:
|
||||
return f()
|
||||
|
||||
|
||||
def _ebAuth(self, ignored, *args):
|
||||
"""
|
||||
Generic callback for a failed authentication attempt. Respond by
|
||||
asking for the list of accepted methods (the 'none' method)
|
||||
"""
|
||||
self.askForAuth('none', '')
|
||||
|
||||
|
||||
def ssh_USERAUTH_SUCCESS(self, packet):
|
||||
"""
|
||||
We received a MSG_USERAUTH_SUCCESS. The server has accepted our
|
||||
authentication, so start the next service.
|
||||
"""
|
||||
self.transport.setService(self.instance)
|
||||
|
||||
|
||||
def ssh_USERAUTH_FAILURE(self, packet):
|
||||
"""
|
||||
We received a MSG_USERAUTH_FAILURE. Payload::
|
||||
string methods
|
||||
byte partial success
|
||||
|
||||
If partial success is C{True}, then the previous method succeeded but is
|
||||
not sufficent for authentication. C{methods} is a comma-separated list
|
||||
of accepted authentication methods.
|
||||
|
||||
We sort the list of methods by their position in C{self.preferredOrder},
|
||||
removing methods that have already succeeded. We then call
|
||||
C{self.tryAuth} with the most preferred method.
|
||||
|
||||
@param packet: the L{MSG_USERAUTH_FAILURE} payload.
|
||||
@type packet: C{str}
|
||||
|
||||
@return: a L{defer.Deferred} that will be callbacked with C{None} as
|
||||
soon as all authentication methods have been tried, or C{None} if no
|
||||
more authentication methods are available.
|
||||
@rtype: C{defer.Deferred} or C{None}
|
||||
"""
|
||||
canContinue, partial = getNS(packet)
|
||||
partial = ord(partial)
|
||||
if partial:
|
||||
self.authenticatedWith.append(self.lastAuth)
|
||||
|
||||
def orderByPreference(meth):
|
||||
"""
|
||||
Invoked once per authentication method in order to extract a
|
||||
comparison key which is then used for sorting.
|
||||
|
||||
@param meth: the authentication method.
|
||||
@type meth: C{str}
|
||||
|
||||
@return: the comparison key for C{meth}.
|
||||
@rtype: C{int}
|
||||
"""
|
||||
if meth in self.preferredOrder:
|
||||
return self.preferredOrder.index(meth)
|
||||
else:
|
||||
# put the element at the end of the list.
|
||||
return len(self.preferredOrder)
|
||||
|
||||
canContinue = sorted([meth for meth in canContinue.split(',')
|
||||
if meth not in self.authenticatedWith],
|
||||
key=orderByPreference)
|
||||
|
||||
log.msg('can continue with: %s' % canContinue)
|
||||
return self._cbUserauthFailure(None, iter(canContinue))
|
||||
|
||||
|
||||
def _cbUserauthFailure(self, result, iterator):
|
||||
if result:
|
||||
return
|
||||
try:
|
||||
method = iterator.next()
|
||||
except StopIteration:
|
||||
self.transport.sendDisconnect(
|
||||
transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
|
||||
'no more authentication methods available')
|
||||
else:
|
||||
d = defer.maybeDeferred(self.tryAuth, method)
|
||||
d.addCallback(self._cbUserauthFailure, iterator)
|
||||
return d
|
||||
|
||||
|
||||
def ssh_USERAUTH_PK_OK(self, packet):
|
||||
"""
|
||||
This message (number 60) can mean several different messages depending
|
||||
on the current authentication type. We dispatch to individual methods
|
||||
in order to handle this request.
|
||||
"""
|
||||
func = getattr(self, 'ssh_USERAUTH_PK_OK_%s' %
|
||||
self.lastAuth.replace('-', '_'), None)
|
||||
if func is not None:
|
||||
return func(packet)
|
||||
else:
|
||||
self.askForAuth('none', '')
|
||||
|
||||
|
||||
def ssh_USERAUTH_PK_OK_publickey(self, packet):
|
||||
"""
|
||||
This is MSG_USERAUTH_PK. Our public key is valid, so we create a
|
||||
signature and try to authenticate with it.
|
||||
"""
|
||||
publicKey = self.lastPublicKey
|
||||
b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) +
|
||||
NS(self.user) + NS(self.instance.name) + NS('publickey') +
|
||||
'\x01' + NS(publicKey.sshType()) + NS(publicKey.blob()))
|
||||
d = self.signData(publicKey, b)
|
||||
if not d:
|
||||
self.askForAuth('none', '')
|
||||
# this will fail, we'll move on
|
||||
return
|
||||
d.addCallback(self._cbSignedData)
|
||||
d.addErrback(self._ebAuth)
|
||||
|
||||
|
||||
def ssh_USERAUTH_PK_OK_password(self, packet):
|
||||
"""
|
||||
This is MSG_USERAUTH_PASSWD_CHANGEREQ. The password given has expired.
|
||||
We ask for an old password and a new password, then send both back to
|
||||
the server.
|
||||
"""
|
||||
prompt, language, rest = getNS(packet, 2)
|
||||
self._oldPass = self._newPass = None
|
||||
d = self.getPassword('Old Password: ')
|
||||
d = d.addCallbacks(self._setOldPass, self._ebAuth)
|
||||
d.addCallback(lambda ignored: self.getPassword(prompt))
|
||||
d.addCallbacks(self._setNewPass, self._ebAuth)
|
||||
|
||||
|
||||
def ssh_USERAUTH_PK_OK_keyboard_interactive(self, packet):
|
||||
"""
|
||||
This is MSG_USERAUTH_INFO_RESPONSE. The server has sent us the
|
||||
questions it wants us to answer, so we ask the user and sent the
|
||||
responses.
|
||||
"""
|
||||
name, instruction, lang, data = getNS(packet, 3)
|
||||
numPrompts = struct.unpack('!L', data[:4])[0]
|
||||
data = data[4:]
|
||||
prompts = []
|
||||
for i in range(numPrompts):
|
||||
prompt, data = getNS(data)
|
||||
echo = bool(ord(data[0]))
|
||||
data = data[1:]
|
||||
prompts.append((prompt, echo))
|
||||
d = self.getGenericAnswers(name, instruction, prompts)
|
||||
d.addCallback(self._cbGenericAnswers)
|
||||
d.addErrback(self._ebAuth)
|
||||
|
||||
|
||||
def _cbSignedData(self, signedData):
|
||||
"""
|
||||
Called back out of self.signData with the signed data. Send the
|
||||
authentication request with the signature.
|
||||
|
||||
@param signedData: the data signed by the user's private key.
|
||||
@type signedData: C{str}
|
||||
"""
|
||||
publicKey = self.lastPublicKey
|
||||
self.askForAuth('publickey', '\x01' + NS(publicKey.sshType()) +
|
||||
NS(publicKey.blob()) + NS(signedData))
|
||||
|
||||
|
||||
def _setOldPass(self, op):
|
||||
"""
|
||||
Called back when we are choosing a new password. Simply store the old
|
||||
password for now.
|
||||
|
||||
@param op: the old password as entered by the user
|
||||
@type op: C{str}
|
||||
"""
|
||||
self._oldPass = op
|
||||
|
||||
|
||||
def _setNewPass(self, np):
|
||||
"""
|
||||
Called back when we are choosing a new password. Get the old password
|
||||
and send the authentication message with both.
|
||||
|
||||
@param np: the new password as entered by the user
|
||||
@type np: C{str}
|
||||
"""
|
||||
op = self._oldPass
|
||||
self._oldPass = None
|
||||
self.askForAuth('password', '\xff' + NS(op) + NS(np))
|
||||
|
||||
|
||||
def _cbGenericAnswers(self, responses):
|
||||
"""
|
||||
Called back when we are finished answering keyboard-interactive
|
||||
questions. Send the info back to the server in a
|
||||
MSG_USERAUTH_INFO_RESPONSE.
|
||||
|
||||
@param responses: a list of C{str} responses
|
||||
@type responses: C{list}
|
||||
"""
|
||||
data = struct.pack('!L', len(responses))
|
||||
for r in responses:
|
||||
data += NS(r.encode('UTF8'))
|
||||
self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data)
|
||||
|
||||
|
||||
def auth_publickey(self):
|
||||
"""
|
||||
Try to authenticate with a public key. Ask the user for a public key;
|
||||
if the user has one, send the request to the server and return True.
|
||||
Otherwise, return False.
|
||||
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
d = defer.maybeDeferred(self.getPublicKey)
|
||||
d.addBoth(self._cbGetPublicKey)
|
||||
return d
|
||||
|
||||
|
||||
def _cbGetPublicKey(self, publicKey):
|
||||
if not isinstance(publicKey, keys.Key): # failure or None
|
||||
publicKey = None
|
||||
if publicKey is not None:
|
||||
self.lastPublicKey = publicKey
|
||||
self.triedPublicKeys.append(publicKey)
|
||||
log.msg('using key of type %s' % publicKey.type())
|
||||
self.askForAuth('publickey', '\x00' + NS(publicKey.sshType()) +
|
||||
NS(publicKey.blob()))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def auth_password(self):
|
||||
"""
|
||||
Try to authenticate with a password. Ask the user for a password.
|
||||
If the user will return a password, return True. Otherwise, return
|
||||
False.
|
||||
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
d = self.getPassword()
|
||||
if d:
|
||||
d.addCallbacks(self._cbPassword, self._ebAuth)
|
||||
return True
|
||||
else: # returned None, don't do password auth
|
||||
return False
|
||||
|
||||
|
||||
def auth_keyboard_interactive(self):
|
||||
"""
|
||||
Try to authenticate with keyboard-interactive authentication. Send
|
||||
the request to the server and return True.
|
||||
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
log.msg('authing with keyboard-interactive')
|
||||
self.askForAuth('keyboard-interactive', NS('') + NS(''))
|
||||
return True
|
||||
|
||||
|
||||
def _cbPassword(self, password):
|
||||
"""
|
||||
Called back when the user gives a password. Send the request to the
|
||||
server.
|
||||
|
||||
@param password: the password the user entered
|
||||
@type password: C{str}
|
||||
"""
|
||||
self.askForAuth('password', '\x00' + NS(password))
|
||||
|
||||
|
||||
def signData(self, publicKey, signData):
|
||||
"""
|
||||
Sign the given data with the given public key.
|
||||
|
||||
By default, this will call getPrivateKey to get the private key,
|
||||
then sign the data using Key.sign().
|
||||
|
||||
This method is factored out so that it can be overridden to use
|
||||
alternate methods, such as a key agent.
|
||||
|
||||
@param publicKey: The public key object returned from L{getPublicKey}
|
||||
@type publicKey: L{keys.Key}
|
||||
|
||||
@param signData: the data to be signed by the private key.
|
||||
@type signData: C{str}
|
||||
@return: a Deferred that's called back with the signature
|
||||
@rtype: L{defer.Deferred}
|
||||
"""
|
||||
key = self.getPrivateKey()
|
||||
if not key:
|
||||
return
|
||||
return key.addCallback(self._cbSignData, signData)
|
||||
|
||||
|
||||
def _cbSignData(self, privateKey, signData):
|
||||
"""
|
||||
Called back when the private key is returned. Sign the data and
|
||||
return the signature.
|
||||
|
||||
@param privateKey: the private key object
|
||||
@type publicKey: L{keys.Key}
|
||||
@param signData: the data to be signed by the private key.
|
||||
@type signData: C{str}
|
||||
@return: the signature
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return privateKey.sign(signData)
|
||||
|
||||
|
||||
def getPublicKey(self):
|
||||
"""
|
||||
Return a public key for the user. If no more public keys are
|
||||
available, return C{None}.
|
||||
|
||||
This implementation always returns C{None}. Override it in a
|
||||
subclass to actually find and return a public key object.
|
||||
|
||||
@rtype: L{Key} or L{NoneType}
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def getPrivateKey(self):
|
||||
"""
|
||||
Return a L{Deferred} that will be called back with the private key
|
||||
object corresponding to the last public key from getPublicKey().
|
||||
If the private key is not available, errback on the Deferred.
|
||||
|
||||
@rtype: L{Deferred} called back with L{Key}
|
||||
"""
|
||||
return defer.fail(NotImplementedError())
|
||||
|
||||
|
||||
def getPassword(self, prompt = None):
|
||||
"""
|
||||
Return a L{Deferred} that will be called back with a password.
|
||||
prompt is a string to display for the password, or None for a generic
|
||||
'user@hostname's password: '.
|
||||
|
||||
@type prompt: C{str}/C{None}
|
||||
@rtype: L{defer.Deferred}
|
||||
"""
|
||||
return defer.fail(NotImplementedError())
|
||||
|
||||
|
||||
def getGenericAnswers(self, name, instruction, prompts):
|
||||
"""
|
||||
Returns a L{Deferred} with the responses to the promopts.
|
||||
|
||||
@param name: The name of the authentication currently in progress.
|
||||
@param instruction: Describes what the authentication wants.
|
||||
@param prompts: A list of (prompt, echo) pairs, where prompt is a
|
||||
string to display and echo is a boolean indicating whether the
|
||||
user's response should be echoed as they type it.
|
||||
"""
|
||||
return defer.fail(NotImplementedError())
|
||||
|
||||
|
||||
MSG_USERAUTH_REQUEST = 50
|
||||
MSG_USERAUTH_FAILURE = 51
|
||||
MSG_USERAUTH_SUCCESS = 52
|
||||
MSG_USERAUTH_BANNER = 53
|
||||
MSG_USERAUTH_INFO_RESPONSE = 61
|
||||
MSG_USERAUTH_PK_OK = 60
|
||||
|
||||
messages = {}
|
||||
for k, v in locals().items():
|
||||
if k[:4]=='MSG_':
|
||||
messages[v] = k
|
||||
|
||||
SSHUserAuthServer.protocolMessages = messages
|
||||
SSHUserAuthClient.protocolMessages = messages
|
||||
del messages
|
||||
del v
|
||||
|
||||
# Doubles, not included in the protocols' mappings
|
||||
MSG_USERAUTH_PASSWD_CHANGEREQ = 60
|
||||
MSG_USERAUTH_INFO_REQUEST = 60
|
||||
Loading…
Add table
Add a link
Reference in a new issue